Closed7

壊れたJSONを修正してくれる「json_repair」を試す

kun432kun432

ここで知った

https://twitter.com/rohanpaul_ai/status/1847986855100174822

GitHubレポジトリ

https://github.com/mangiucugna/json_repair

このシンプルなパッケージは、無効なjson文字列を修正するために使用できます。このパッケージが動作するすべてのケースを知るには、ユニットテストを確認してください。

https://github.com/josdejong/jsonrepair にインスパイアされています

##デモ

このライブラリがあなたの特定の問題を解決できるかどうか不明な場合、または単にオンラインでjsonを検証したい場合は、GitHubページのデモサイト( https://mangiucugna.github.io/json_repair/ )にアクセスしてください。

モチベーション

LLMに整ったJSONデータを返させるとなると、LLMは少し不安定になります。時には括弧を省略し、時にはそこにいくつかの単語を追加します。幸いにも、LLMが犯すミスはコンテンツを破壊することなく修正できるほど単純なものです。

この問題を確実に修正できる軽量なPythonパッケージを探しましたが、見つかりませんでした。

そこで、自分で作成しました

GPT-4o Structure Outputによって、このライブラリは時代遅れになるのではないでしょうか?

私たちの仕事の一部として、OpenAIのAPIを使用していますが、構造化された出力でも、結果が完全に有効なJSONではない場合があることに気づきました。そのため、私たちは今でもこのライブラリを使用して、そのような例外に対応しています。

サポートされるユースケース

JSONの構文エラーの修正

  • 引用符の欠落、カンマの誤った位置、エスケープされていない文字、不完全なキーと値のペア。
  • 引用符の欠落、不適切なフォーマットの値(true、false、null)、破損したキーと値の構造の修復。

不正なJSON配列とオブジェクトの修復

  • 必要な要素(カンマ、括弧など)やデフォルト値(null、"")を追加することで、不完全または破損した配列/オブジェクトを修復します。
  • このライブラリは、コメントや不適切な位置に配置された文字などの余分な非JSON文字を含むJSONを処理することができ、有効な構造を維持しながらそれらをクリーンアップします。

欠落したJSON値の自動補完

  • 妥当なデフォルト値(空文字列やnullなど)で、JSONフィールド内の欠落した値を自動的に補完し、有効性を確保します。

なるほど、Structured Outputでもダメなケースはまだまだあるのか。あとローカルLLMとかでも役に立ちそうな気がする。

kun432kun432

Colaboratoryで。

パッケージインストール

!pip install json-repair

で、json_repairを使う前に、まずは自分でJSONスキーマをPydanticオブジェクトでバリデーションするサンプル。

from pydantic import BaseModel, ValidationError
from typing import List, Optional
import json

# Pydanticモデルの定義
class UserModel(BaseModel):
    name: str
    age: int
    email: Optional[str] = None
    hobbies: List[str]

# JSONバリデーション関数
def validate_json(json_str: str):
    try:
        json_data = json.loads(json_str)
        
        item = UserModel(**json_data)
        
        print("Validation Success!\n")
        print(json.dumps(item.dict(), indent=2, ensure_ascii=False))
    
    except Exception as e:
        if isinstance(e, json.JSONDecodeError):
            print(f"JSONDecodeError: {e.msg}\n")
            print(f"Error at line {e.lineno}, column {e.colno}")
        elif isinstance(e, ValidationError):
            print("Validation Error!\n")
            print(json.dumps(json.loads(e.json()), indent=2, ensure_ascii=False))
        else:
            print(f"Unexpected error: {str(e)}")

まず正しいJSONで試してみる。

json_str = """
{
    "name": "山田太郎",
    "age": 30,
    "email": "yamada@example.com",
    "hobbies": ["読書", "ゲーム"]
}
"""

validate_json(json_str)
Validation Success!

{
  "name": "山田太郎",
  "age": 30,
  "email": "yamada@example.com",
  "hobbies": [
    "読書",
    "ゲーム"
  ]
}

キーが1つ足りないJSONでも試してみる。

# nameがない
json_str = """
{
    "age": 30,
    "email": "yamada@example.com",
    "hobbies": ["読書", "ゲーム"]
}
"""

validate_json(json_str)
Validation Error!

[
  {
    "type": "missing",
    "loc": [
      "name"
    ],
    "msg": "Field required",
    "input": {
      "age": 30,
      "email": "yamada@example.com",
      "hobbies": [
        "読書",
        "ゲーム"
      ]
    },
    "url": "https://errors.pydantic.dev/2.9/v/missing"
  }
]

文法的に破綻したJSON

# ケツカンマ
json_str = """
{
    "name": "山田太郎",
    "age": 30,
    "email": "yamada@example.com",
    "hobbies": ["読書", "ゲーム"],
}
"""

validate_json(json_str)
JSONDecodeError: Expecting ',' delimiter

Error at line 5, column 5

こんな感じで、

  • スキーマに合致しているかはPydanticでバリデーションする
  • JSON自体が破綻しているかどうかはjson.loadsの例外を拾う

ことはできるけど、エラーを拾えるというところまでになる。

json_repairを使うとどうやら後者については自動的に修正してくれる様子。ユースケースを見るともうちょっと踏み込んだ感じもできるように思えるけど、一旦は思いつくところでいろいろ試してみる。

その1: ケツカンマ

from json_repair import repair_json

bad_json_str = """
{
    "name": "山田太郎",
    "age": 30,
    "email": "yamada@example.com",
    "hobbies": ["読書", "ゲーム"],
}
"""

good_json_string = repair_json(bad_json_str)

validate_json(good_json_string)
Validation Success!

{
  "name": "山田太郎",
  "age": 30,
  "email": "yamada@example.com",
  "hobbies": [
    "読書",
    "ゲーム"
  ]
}

その2: 途中のカンマ抜け

from json_repair import repair_json

bad_json_str = """
{
    "name": "山田太郎",
    "age": 30
    "email": "yamada@example.com",
    "hobbies": ["読書", "ゲーム"],
}
"""

good_json_string = repair_json(bad_json_str)

validate_json(good_json_string)

その3: クォート閉じ忘れ

from json_repair import repair_json

bad_json_str = """
{
    "name": "山田太郎,
    "age": 30,
    "email": "yamada@example.com",
    "hobbies": ["読書", "ゲーム"],
}
"""

good_json_string = repair_json(bad_json_str)

validate_json(good_json_string)
Validation Success!

{
  "name": "山田太郎",
  "age": 30,
  "email": "yamada@example.com",
  "hobbies": [
    "読書",
    "ゲーム"
  ]
}

その4: カッコ閉じ忘れ

from json_repair import repair_json

bad_json_str = """
{
    "name": "山田太郎",
    "age": 30,
    "email": "yamada@example.com",
    "hobbies": ["読書", "ゲーム"
}
"""

good_json_string = repair_json(bad_json_str)

validate_json(good_json_string)
Validation Success!

{
  "name": "山田太郎",
  "age": 30,
  "email": "yamada@example.com",
  "hobbies": [
    "読書",
    "ゲーム"
  ]
}
kun432kun432

json_repairはjson.loads()を置き換えるjson_repair.loads()を提供しているので、最初のコードも以下のように書き換えることができる。

from pydantic import BaseModel, ValidationError
from typing import List, Optional
import json
import json_repair

# Pydanticモデルの定義
class UserModel(BaseModel):
    name: str
    age: int
    email: Optional[str] = None
    hobbies: List[str]

# json_repairを使ったJSONバリデーション関数
def validate_json(json_str: str):
    try:
        safe_json_data = json_repair.loads(json_str)
        
        item = UserModel(**safe_json_data)
        
        print("Validation Success!\n")
        print(json.dumps(item.dict(), indent=2, ensure_ascii=False))
    
    except Exception as e:
        if isinstance(e, ValidationError):
            print("Validation Error!\n")
            print(json.dumps(json.loads(e.json()), indent=2, ensure_ascii=False))
        else:
            print(f"Unexpected error: {str(e)}")

json_repairが少なくともJSONとしての文法は正しくしてくれるので、json.JSONDecodeErrorは不要になるということか。

なお、同じことをrepair_jsonでやるならこうなる。

(snip)
    try:
        safe_json_data = json_repair.repair_json(json_str, return_objects=True)
        
        item = UserModel(**safe_json_data)
(snip)
kun432kun432

JSON「ファイル」の修正

不正なJSONファイルを作成

%%writefile bad_json_sample.json
{
    # コメント入り
    "name": "山田太郎",
    # カンマ抜け
    "age": 30
    # カッコ閉じ忘れ
    "hobbies": ["読書", 
    # ケツカンマ
    "email": "yamada@example.com",
}

ファイルオブジェクトで渡す、サンプルのようにrbで開いてjson_repair.loadに渡すと空文字列になってしまったので、少し書き換えている。

import json_repair

with open("bad_json_sample.json", 'r') as f:
    cleaned_json_from_file = json_repair.load(f)
    print(json.dumps(cleaned_json_from_file, indent=2, ensure_ascii=False))

ファイルをjson_repair.from_file()で開く

import json_repair
import json

cleaned_json_from_file = json_repair.from_file("bad_json_sample.json")
print(json.dumps(cleaned_json_from_file, indent=2, ensure_ascii=False))

どちらも以下となる。

{
  "name": "山田太郎",
  "age": 30,
  "hobbies": [
    "読書",
    "ケツカンマ",
    "email",
    "yamada@example.com"
  ]
}
kun432kun432

CLIもある。

!json_repair -h
usage: json_repair [-h] [-i] [-o TARGET] [--ensure_ascii] [--indent INDENT] filename

Repair and parse JSON files.

positional arguments:
  filename              The JSON file to repair

options:
  -h, --help            show this help message and exit
  -i, --inline          Replace the file inline instead of returning the output to stdout
  -o TARGET, --output TARGET
                        If specified, the output will be written to TARGET filename instead of
                        stdout
  --ensure_ascii        Pass ensure_ascii=True to json.dumps()
  --indent INDENT       Number of spaces for indentation (Default 2)

例えば先ほどのファイルに出力した不正なJSONファイルを食わせてみる。

!json_repair bad_json_sample.json
{
  "name": "山田太郎",
  "age": 30,
  "hobbies": [
    "読書",
    "ケツカンマ",
    "email",
    "yamada@example.com"
  ]
}
このスクラップは2ヶ月前にクローズされました