Snowflake「Error parsing JSON」の原因とSTRIP_OUTER_ARRAY対処法

Snowflake「Error parsing JSON」の原因とSTRIP_OUTER_ARRAY対処法のサムネイル Snowflake
この記事をシェアする𝕏B!FacebookLINEPocket

結論:このエラー、原因はだいたい3パターンしかない

SnowflakeでCOPY INTOPARSE_JSONを叩いたら、いきなり Error parsing JSON で落ちた。検索でここに辿り着いたなら、まず安心してください。このエラー、実際に手を動かして踏むと分かるのですが、原因は次の3つにほぼ集約されます。

  1. ファイルの中身がJSON配列([ {...}, {...} ])で、Snowflake側がそれを1レコードとして読もうとしている
  2. NDJSON(改行区切りJSON)なのに、ファイルフォーマットでJSONを指定していない、もしくはCSV扱いされている
  3. JSON自体が壊れている(末尾カンマ、シングルクォート、BOM、改行コード混入など)

このうち、業務でロードを組んでいて一番頻発するのは断トツで1番。先に答えだけ書いておくと、STRIP_OUTER_ARRAY = TRUE を付ければ大半は通ります。ただ、それで黙らせて終わりにすると後で別の現象で詰むので、ちゃんと中身を確認しておきましょう。

図解:Snowflakeで配列形式JSONをCOPY INTOした際に発生する「Error parsing JSON」の典型原因と、STRIP_OUTER_ARRAY = TRUEを付与して外側の配列を剥がし行単位でロードする対処フローをまとめたイメージ

再現コードとエラー原文

まずは、よくある詰まり方を再現してみます。S3やステージに置いたJSONファイルが、こういう形だったとします。

[
  {"id": 1, "name": "alice"},
  {"id": 2, "name": "bob"},
  {"id": 3, "name": "carol"}
]

これを素直にロードしようとして、こう書いたとします。

CREATE OR REPLACE FILE FORMAT my_json_fmt
  TYPE = JSON;

COPY INTO raw_users
FROM @my_stage/users.json
FILE_FORMAT = (FORMAT_NAME = my_json_fmt);

ところが返ってくるのは、こんなメッセージです。

Error parsing JSON: document is too large, max size 16777216 bytes

あるいは形式違いだとこちらが出ます。

Error parsing JSON: more than one document in the input

このエラー文をそのままGoogleに貼り付けてここに辿り着いた方、たぶん上のどちらかですよね。ちなみにSnowsightのエラー表示、テーブル名やステージ名が問答無用で大文字に化けて返ってくるので、自分の小文字定義と見比べてビクッとしますが、それは別の話です。

原因:Snowflakeは「1ファイル=1ドキュメント」とは限らないと考える

JSONのロード挙動を理解するうえで一番大事なのは、SnowflakeのTYPE = JSONNDJSON(=1行1JSONオブジェクト)を前提に動くという点です。つまり、こういう形が一番きれいに通ります。

{"id": 1, "name": "alice"}
{"id": 2, "name": "bob"}
{"id": 3, "name": "carol"}

ところが世の中のAPIは、ほぼ確実に外側を [ ... ] でくくった「単一の配列」で返してきます。Snowflakeは配列全体を1ドキュメントとして読もうとして、要素数が多いと document is too large、複数の配列が並んでいると more than one document を吐く、という構造です。

そして、これを解決するために用意されているのが STRIP_OUTER_ARRAY オプションです。これをTRUEにすると、ロード時に外側の[ ]を剥がし、内側の各要素を別レコードとしてVARIANTカラムへ展開してくれます。

原因切り分けチェックリスト

解決SQLに行く前に、自分のケースがどれに該当するか、ざっとチェックしましょう。

  1. ファイルが [ で始まっているか? → ローカルでhead -c 200 file.json。先頭が[なら配列形式、{ならオブジェクト形式。
  2. 1行に複数のJSONが連結されていないか? → APIのレスポンスを単純連結したログでよくあります。
  3. BOM()が混入していないか? → Windowsで保存したUTF-8 JSONあるある。fileコマンドで確認できます。
  4. 末尾カンマ、シングルクォート、コメント行 → JSON5やJSONCの記法は通りません。標準JSONに整形する必要があります。
  5. ファイル全体のサイズが16MBを超えていないか? → 単一ドキュメントとして読まれた場合の上限です。STRIP_OUTER_ARRAY適用後の各要素サイズでも16MBを超えると同じエラーが出ます。
SnowflakeでError parsing JSONが発生する主な原因チェックリストと、STRIP_OUTER_ARRAYやBOM混入、16MB上限、末尾カンマなどの確認ポイントを整理し、解決SQLパターンへつなぐ図解

解決方法:パターン別の修正SQL

パターン1: 外側が配列のJSON → STRIP_OUTER_ARRAYを足す

一番多いやつです。ファイルフォーマットを作り直すか、COPY INTO側でその場指定でも構いません。

CREATE OR REPLACE FILE FORMAT my_json_fmt
  TYPE = JSON
  STRIP_OUTER_ARRAY = TRUE;

COPY INTO raw_users
FROM @my_stage/users.json
FILE_FORMAT = (FORMAT_NAME = my_json_fmt);

これで配列内の{"id":1,...}がそれぞれ1レコードになって入ります。VARIANTカラムはraw_users(v VARIANT)のような1列構成にしておくのが定番です。

パターン2: ロード前に内容を確認したい

本番ロード前に一回中身を覗きたい時は、SELECTでステージから直接読めます。これ、地味に便利です。

SELECT $1
FROM @my_stage/users.json
(FILE_FORMAT => 'my_json_fmt')
LIMIT 5;

ここで$1に配列丸ごとが入ってきたら、配列形式確定。STRIP_OUTER_ARRAY = TRUEを付けて再実行すると、要素単位に変わるのが確認できます。

パターン3: PARSE_JSON側でこけている場合

ロードではなく、すでにVARCHARで取り込んでしまった文字列に対して PARSE_JSON をかけたら落ちる、というケース。これはJSON自体が壊れているか、エスケープ漏れの可能性が高いです。TRY_PARSE_JSONに置き換えれば、失敗時にNULLを返すだけになるので、まずどのレコードで落ちているか特定できます。

SELECT id, raw_text
FROM staging_logs
WHERE TRY_PARSE_JSON(raw_text) IS NULL
  AND raw_text IS NOT NULL
LIMIT 100;

引っかかったレコードを目視で見ると、末尾カンマやNaNといったJSON標準外のリテラルが混ざっていることが多いはずです。

パターン4: 1ファイル内にJSONがダラダラ連結されている

APIレスポンスをそのままcatで結合した、みたいなファイルだと more than one document が出ます。これはENABLE_OCTAL = FALSEとか別オプションでは救えません。素直にNDJSON形式(1行1オブジェクト)に整形し直すのが最短です。ロード前処理がない環境なら、SnowflakeのTask + ストアドプロシージャで前処理を挟む手もあります。

Snowsightでの確認手順

Snowsightからやる場合、左メニューの「Data」→「Databases」から該当ステージを開き、「Load Data」ウィザードに進むとファイルフォーマットを画面から設定できます。Strip outer arrayのチェックボックスがそのまま出ているので、初手はここをオンにして試すのが早いです。ちなみに、ウィザードで作ったファイルフォーマットはそのままオブジェクトとして残るので、SHOW FILE FORMATS;で名前を確認して、以降のスクリプトから参照すると運用が楽になります。

余談ですが、Snowsightのワークシートはしばらく放置すると裏でセッションが切れていて、ウィザードから戻った瞬間にUSE WAREHOUSEが外れていることがあります。ロード前にSELECT CURRENT_WAREHOUSE();を一回叩く癖をつけておくと、変なところで詰まらずに済みます。

SnowsightのLoad Dataウィザードでファイルフォーマットを設定する画面のスクリーンショットで、Strip outer arrayチェックボックスをオンにしてJSON配列を分解しSnowflakeへロードする手順を示した解説図

再発防止の運用Tips

一度直っても、次のファイルでまた踏むのがこのエラーの厄介なところです。実務で長くSnowflakeを触っていると、次の運用が効いてきます。

  • ファイルフォーマットは「配列前提」と「NDJSON前提」を2種類用意して命名で分ける。例えばJSON_ARRAY_FMTJSON_NDJSON_FMTのように。命名規則についてはSnowflake命名規則ベストプラクティスもあわせて参考にしてください。
  • COPY INTOには必ずON_ERROR = 'CONTINUE''SKIP_FILE'を一旦付けて検証する。1件目で全停止すると残り何件壊れているか永遠に分かりません。詳しくはCOPY INTO失敗のチェックリストを参照ください。
  • VALIDATION_MODE = RETURN_ERRORSでドライランしておくと、本番ロードの前にエラー行が一覧で返ってきます。
  • VARIANTカラムへ取り込んだ後の型変換はTRY_*系で書くTRY_TO_NUMBERTRY_TO_DATEを活用すると、後段の集計でこける確率が下がります。

まとめ

Error parsing JSONが出たら、まずはファイルの先頭1文字を確認。[で始まっていたらSTRIP_OUTER_ARRAY = TRUE、それでもダメならファイルそのものが壊れていないか、NDJSON形式に整形できないかを順に潰していく――この順番だけ覚えておけば、検索画面に戻る回数はぐっと減るはずです。

VARIANT周りはSnowflakeの強みでもあり、最初のロード段階でつまずきやすいポイントでもあります。一度フォーマット定義をきれいに揃えておくと、次回からは「ステージにファイルを置いてCOPY INTOを叩くだけ」の世界に持っていけます。

参考リンク

関連記事

この記事をシェアする𝕏B!FacebookLINEPocket