今回は、FileMakerのデータをJSONに変換するときに発生する可能性がある問題についての、Soliant Consultingのブログ記事を紹介します。
FileMaker JSON Serialization and Deserialization Gotchas
(元記事はこちら)
Mislav Kos
2019/8/20
FileMaker純正のJSON関数が登場してからしばらく経ち、その結果として、FileMakerソリューション内でJSONが広く使われるようになりました。その一般的な用途は次のとおりです。
- REST APIとの通信
- スクリプト引数と結果の受け渡し
- セッションデータの追跡
- バーチャルリストの表示
- 非永続データの処理
JSONにシリアライズ、または逆にデシリアライズするときに、開発者が注意すべき落とし穴がいくつかあります。(「シリアライズ」とは、FileMakerのフィールドまたは変数の値をJSONデータに変換することです。「デシリアライズ」は逆の操作で、JSONからFileMakerのフィールドまたは変数に値を抽出することです。)注意すべき挙動は、次のようないくつかの原因で起こります。
- FileMakerデータ型とJSONデータ型の不一致
- ファイルのメタデータ
- 改行処理
FileMakerとJSONのデータ型の不一致
JSONデータのBLOB型はJSONオブジェクトまたはJSON配列のいずれかになりますが、FileMakerではテキスト値として表されます。JSONオブジェクト(または配列)内で、値(または配列の値)は、JSONデータ型のいずれかになります。これらのタイプを、以下の表にFileMakerデータタイプとともに示しました。すべてのタイプが必ずしも他方に対応するタイプを持つわけではないことに注意してください。
FileMaker | JSON |
---|---|
— | Object |
— | Array |
テキスト | String |
数字 | Number |
— | Boolean |
日付 | — |
時刻 | — |
タイムスタンプ | — |
オブジェクト | — |
— | null |
FileMakerのフィールドには明示的なデータ型がありますが、FileMakerの変数の挙動はそれほど明白ではありません。FileMaker計算エンジンが変数のデータ型を決定する方法の正確なルールは文書化されていませんが、変数は変数に割り当てられた値からデータ型を継承しているようです。たとえば、テキスト値を割り当てた場合、変数のデータ型はテキストになります。これはほとんどの場合は問題なく機能しますが、問題を引き起こす可能性のあるエッジケース(値が限界ぎりぎりなどで、特別な問題を含む可能性がある状況)がいくつかあります。以下にその例を示します。
シリアライズとデシリアライズは、テキスト«»文字列などの一部のデータ型では期待どおりに問題なく機能するため、ここでは取り上げていません。代わりに、自明でない挙動に焦点を当てます。
数値の精度
FileMakerは、非常に大きな数字と非常に高い精度の数字の両方をサポートしています。FileMaker Pro 18 Advancedの技術仕様によると、サポートされている値は、10 ^ -400から10 ^ 400までと、それと同じ範囲の負の値です。
FileMakerで使われるJSONの実装では、同じ範囲がサポートされません。数値が範囲を超えると、JSON文字列にシリアライズされるときに科学的記数法(指数を使った表記法)に変換されるか切り捨てられます。これにより、精度が失われます。いくつかの例を見てみましょう。
以下の18桁の数値は正しくシリアライズされます。
JSONSetElement ( "" ; "x" ; 123456789012345678 ; JSONNumber ) = {"x":123456789012345678}
しかし、以下の19桁の数値は、シリアライズされると科学的記数法に変換されます。
$json = JSONSetElement ( "" ; "x" ; 1234567890123456789 ; JSONNumber ) = {"x":1.23456789012346e+18}
その結果、この数値をデシリアライズすると精度が失われます。
JSONGetElement ( $json ; "x" ) = 1.23456789012346e+18 = 1234567890123460000 ≠ 1234567890123456789
同様に、15桁の精度のこの数値は正しくシリアライズされます。
JSONSetElement ( "" ; "x" ; 0.123456789012121 ; JSONNumber ) = {"x":0.123456789012121}
そして、以下の16桁の精度の数値は、精度が失われてシリアライズされます。
JSONSetElement ( "" ; "x" ; 0.1234567890121212 ; JSONNumber ) = {"x":0.123456789012121}
これは、大きな数値や高精度の数値を扱う場合に問題になりますが、通常はレコードのデータにそのような数値は含まれません。より一般的な使用例は、UUID番号を使う場合です。UUID番号をIDフィールドに割り当てて、JSONを使ってIDフィールドの値をスクリプトに渡すと、その値が変わります。
精度の損失の問題を回避するには、JSONNumberではなくJSONStringとして数値をシリアライズします。
型変換 (typecast)
数値が変更されないよう、数値をJSONStringとしてシリアライズした場合、デシリアライズ時に値を数値として型変換する必要があります。型変換(typecast)とは、データを解釈する型を明示的に宣言するプロセスを指します。FileMakerでは、GetAs系の関数を使ってこれを行うことができます。
$ourNumber = GetAsNumber ( JSONGetElement ( $json ; "ourNumber" ) )
なぜこれが必要なのでしょうか? たとえば数値がJSONString “10”であるとします。デシリアライズ時に型変換しない場合、FileMakerはこの値をテキストとして扱い、予期しない動作を引き起こす可能性があります。
$ourNumber < 2 // 1(True)を返す
これは以下の式と同じです。
"10" < 2 // 1(True)を返す
2つの値の大小を比較するとき、それらの値の1つがテキストの場合、FileMakerはテキストベースの比較(collationと呼ばれます)を実行します。そこではまず、それぞれの値の最初の1文字を比較します。それらが同じ場合、2番目の文字を比較し、以下同様に続きます。今回の場合、最初の文字(“1”)は2よりも前(つまり2よりも小さい)であるため、Falseであると期待していたところが、結果はTrueが返されます。
空の数値
空の値がJSONNumberとしてシリアライズされる場合、値は0に設定されます。空やnullにはなりません。
JSONSetElement ( "" ; "x" ; $emptyNumber ; JSONNumber ) = {"x":0}
これは大した問題ではないと思えるかもしれませんが、0と空は同じではありません。この値をFileMakerの変数またはフィールドにデシリアライズし、IsEmpty()で値が空かどうかをテストすると、結果はFalseになります。シリアライズ/デシリアライズ前にテストした場合とは異なります。
回避策は上と同じです。空の値を保持するには、数値もJSONStringとしてシリアライズします。デシリアライズするときは、忘れずにGetAsNumber()で値を型キャストしてください。(そうすれば空の値は0になりません。)
または、値をJSONNullとしてシリアライズする方法もあります。しかし、これは式のロジックを複雑にします。
JSONSetElement ( "" ; "x" ; $emptyNumber ; if ( IsEmpty ( $emptyNumber ) ; JSONNull ; JSONNumber ) )
日付、時刻、タイムスタンプ
FileMakerの日付、時刻、タイムスタンプには、対応するJSONデータ型がありません。これらの値をNumberまたはStringとしてシリアライズできますが、Numberを使うと人間が理解できない表現になります。つまり”10/1/2019″は人間にとって意味がありますが、737333 (FileMakerが10/1/2019を数値として表示したもの) は理解できません。
10/1/2019をJSONStringとしてシリアライズしてからデシリアライズする場合は、日付として型変換する必要があります。これを行わないと、FileMakerはそれをテキストとして扱います。これは、上でJSONStringとして保存した数値をデシリアライズしたときと同じです。そして同じように、大小比較は想定とは異なる挙動をします。
これを確認するには、データビューアで次の計算式を入力してください。
Let ( [ earlierDate = Date ( 2 ; 1 ; 2019 ) ; laterDate = Date ( 10 ; 1 ; 2019 ) ; json = JSONSetElement ( "" ; "key" ; laterDate ; JSONString ) ; laterDateFromJSON = JSONGetElement ( json ; "key" ) ] ; List ( earlierDate < laterDateFromJSON ; // 0(False)を返す earlierDate < GetAsDate ( laterDateFromJSON ) ; // 1(True)を返す ) )
この例のリストの1つ目は、機能的には以下と同じです。
GeAsDate ("2/1/2019") < "10/1/2019" // 0(False)を返す
式の右側にテキスト値があるので、大小比較はテキストベースで行われ、FebruaryがOctoberより前(つまり小)であっても式はFalseと評価されます。
時刻とタイムスタンプの値には、日付と同じ問題があります。これらの問題を回避するには、日付、時刻、タイムスタンプはJSONStringとしてシリアライズし、デシリアライズ時にはGetAsDate、GetAsTime、GetAsTimestampを使って値を型変換します。
ファイルのメタデータ
JSONはバイナリ値を処理できませんが、FileMakerのオブジェクトフィールド(または変数)はbase64でエンコードしてJSONStringとしてシリアライズできます。これによってファイルの内容は保持されますが、ファイルメタデータ(ファイル名や作成日など)は失われます。
ですが、ファイル名は次の方法で簡単に保存できます。
$json = JSONSetElement ( "" ; [ "myFileName" ; GetContainerAttribute ( $myFile ; "filename" ) ; JSONString ] ; [ "myFileContents" ; Base64EncodeRFC ( 2045 ; $myFile ) ; JSONString ] )
これは、次のようにデシリアライズできます。
Base64Decode ( JSONGetElement ( $json ; "myFileContents" ) ; JSONGetElement ( $json ; "myFileName" ) )
しかし、FileMakerが提供するツールを使って他のメタデータを回復することはできません。たとえば、FileMaker Goで署名がキャプチャされると、タイムスタンプがメタデータとして画像に埋め込まれます。この情報は、ファイル名で行ったのと同じようにシリアライズできます。
$json = JSONSetElement ( "" ; [ "sigFileName" ; GetContainerAttribute ( $myFile ; "filename" ) ; JSONString ] ; [ "sigTimestamp" ; GetContainerAttribute ( $myFile ; "signed" ) ; JSONString ] ; [ "sigFileContents" ; Base64EncodeRFC ( 2045 ; $myFile ) ; JSONString ] )
しかし、標準のFileMaker関数では署名のタイムスタンプのメタデータを使ってファイルを再構築することはできません。
オブジェクトフィールドのデータをREST APIに送信し、そのAPIが付属するメタデータでファイルを正しく再構築できる場合、これは問題ではありません。しかし、たとえばスクリプト引数の一部として、オブジェクトフィールドのデータを別のFileMakerファイルに渡す場合は、この制約を他の方法で補う必要があります。
改行処理
オブジェクトフィールドのデータがbase64でエンコードされているときに、場合によっては(使用されているbase64エンコードのタイプに応じて)、結果のテキストは複数行に分割されます。エンコードタイプはRFC番号によって管理されます。詳細については、このヘルプ記事を参照してください。たとえば、RFC #2045タイプのbase64エンコーディングを使用する場合、行は1行あたり最大76文字で分割されます。
$base64 = Base64EncodeRFC ( 2045 ; $myContainer ) JVBERi0xLjMKJcTl8uXrp/Og0MTGCjQgMCBvYmoKPDwgL0xlbmd0aCA1IDAgUiAvRmlsdGVyIC9G bGF0ZURlY29kZSA+PgpzdHJlYW0KeAGNkTFPxDAMhff8ije2SOk1idu0Y+/gBiZOisSAmCpODFek 0v8v4aSJAYkBZfAn23mxX1ZcsMIMoBad7+Ed4fMNz/jA4bQZzBtMOtscu7wnLOhGSnRLxDdvXFM5 ... MTA1PiBdID4+CnN0YXJ0eHJlZgoxMzM0NQolJUVPRgo=
データを複数行に分割するエンコードタイプにはCRLF改行を使用します。つまり、各行はcarriage return(CR)とそれに続くline feed(LF)で区切られます。CRLF改行文字は、コード1000013に対応する単一のUTF-16文字で表されます。
Code ( Middle ( $base64 ; 77 ; 1 ) ) = 1000013
このbase64でエンコードされたテキストがJSONにシリアライズされると、CRLF改行文字は\rと\nに変換されます。
$base64JSON = JSONSetElement ( "" ; "myContainer" ; $base64 ; JSONString ) {"myContainer":"JVBERi0xLjMKJcTl8uXrp/Og0MTGCjQgMCBvYmoKPDwgL0xlbmd0aCA1IDAgUiAvRmlsdGVyIC9G\r\nbGF0ZURlY29kZSA+PgpzdHJlYW0KeAGNkTFPxDAMhff8ije2SOk1idu0Y+/gBiZOisSAmCpODFek\r\n0v8v4aSJAYkBZfAn23mxX1ZcsMIMoBad7+Ed4fMNz/jA4bQZzBtMOtscu7wnLOhGSnRLxDdvXFM5\r\n...MTA1PiBdID4+CnN0YXJ0eHJlZgoxMzM0NQolJUVPRgo=\r\n"}
ただし、逆のプロセスでは、\r\n文字はCRのみ(コード13)としてデシリアライズされます。
Code ( Middle ( JSONGetElement ( $base64JSON ; "myContainer" ) ; 77 ; 1 ) ) = 13
つまり、CRLF改行は失われ、代わりにCRが使われます。
この挙動は、Base64Decode関数を使ってファイルを再構築するときに問題を引き起こすようには見えません。以下は正常に機能します。
Base64Decode ( JSONGetElement ( $base64JSON ; "myContainer" ) ; "myFile.txt" )
ただし、デシリアライズされたbase64エンコード値を受け取る側が (Base64Decode関数とは異なり) CRLF改行を想定してCR改行を受け付けない場合、問題が発生する可能性があります。
まとめ
自明でないシリアライズおよびデシリアライズの動作が発生するのは、エッジケースの場合のみです。通常の使用では、このような状況は発生しません。しかし、これらはまさにデバッグが難しいエラーに属します。
これらに備えるには、JSONをデシリアライズするときに値を明示的に型変換する習慣を身に付けましょう。数値の場合もJSONStringを使うことを検討してください。また、オブジェクトフィールドのデータを扱う場合は、base64エンコードにファイルメタデータが含まれていないこと、デシリアライズ時にはFileMakerがCRLF改行をCRに変換することに注意してください。