謙虚な気持ちでJSONの仕様と各言語での実装例を調べなおす

Table of Content

JSONの仕様

JavaScript Object NotationはJavaScriptから派生したものですが、2019年現在、多くのプログラミング言語にてJSON形式のデータを生成および解析するためのコードが含まれています。

仕様改定の歴史としては以下のようになっています。

時系列 仕様 備考
2006年7月 RFC4627
2013年3月 RFC7158 RFC4627ではJSONの文章はobjectまたはarrayを必ず含む必要がありましたが、その制限が解除されました。
2013年10月 ECMA-404 1st edition
2014年3月 RFC7159
2017年12月14日 RFC8259およびIETF STD 90およびECMA-404 2nd edition RFC8259にて文字コードはBOMなしのUTF-8でエンコードすることが必須となりました。

JSON values

JSONの値はobject, array, number, string, true, false, null となります。

Object

objectは0個以上の名前/値のペアを囲む、一対のトークンとして表せられます。名前はstringになります。名前の後に「:」とトークンが続き、名前と値を分けます。単一の「,」は値を次の名前から分離します。
名前と値のペアの順序に重要性はありませんし、名前文字列が一意であることを要求するものではありませんし、複数あった場合の挙動は定義されていません。

Array

arrayは0個以上の値を囲む角括弧トークンです。値は「,」で区切ります。

Number

numberは余分の先頭0が存在しない10進数です。
マイナス記号、小数点、eまたはEが付与される場合があります。
数字として表すことのできないNaNやInfinityは許可されません。

10 → Numberとみなす
-10 → Numberとみなす
10.5 → Numberとみなす
1e+3 → Numberとみなす
1E-3 → Numberとみなす
-0 → Numberとみなす
0010 → Numberとみなさない
0x10 → Numberとみなさない
NaN → Numberとみなさない

String

stringは「\」によるエスケープシーケンス記法を含む、「"」でくくった文字列です。

\u4 hexadecimal digitsの例は以下のようになります。
・"\u3041"→"ぁ"となります。
・"\u002F"と"\u002f"は両方とも有効

実装例

ここでは以下のプログラミング言語で実際にJSONを操作してみます。
・JavaScript
・Python
・C#
・PowerShell
・Excel VBA

操作対象のJSONの例

いろいろなValueを読み込むJSON

test001.json

{
  "null" : null,
  "num1" : 12345,
  "num1" : 99999,
  "num2" : 123.45,
  "num3" : -123.45,
  "num4" : 1e+3,
  "num5" : 1E-3,
  "bool1" : true,
  "bool2" : false,
  "str1" : "abあいうえおcdef",
  "str2" : "str2:\n\r\\\t\/abc\b\u0030\u3041",
  "obj1" : {
    "a" : 125,
    "b" : {
       "c" : {
          "d1": {
             "v" : 1234
          },
          "d2" : 12345
       }
    }
  },
  "ary1" : [
    1,
    2,
    "test",
    {
      "a": 5.3
    }
  ],
  "ary2" : [
    1,
    2,
    3
  ]
}

大量データのJSON

test003.json

[
  { "num1" : 1, "num2" : 2 },
  { "num1" : 2, "num2" : 2 },
  { "num1" : 3, "num2" : 2 },
  { "num1" : 4, "num2" : 2 },
  { "num1" : 5, "num2" : 2 },
  // 数百メガバイトになるまで繰り返し
]

JavaScript

環境:
Windows10
node.js v10.16.0

標準のJSONオブジェクト

標準のJSONオブジェクトのparseとstringifyでJSONの操作が行えます。
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON

var fs = require("fs");
var contents = fs.readFileSync("C:\\share\\jsontest\\test001.json");
var jsonData = JSON.parse(contents);
console.log(jsonData['null']);
console.log(jsonData['num1']);
console.log(jsonData['num2']);
console.log(jsonData['num3']);
console.log(jsonData['num4']);
console.log(jsonData['num5']);
console.log(jsonData['str1']);
console.log(jsonData['str2']);
console.log(jsonData['obj1']);
console.log(jsonData['obj1']['b']['c']);
console.log(jsonData['ary1']);
console.log(jsonData['ary1'][0]);

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
console.log(JSON.stringify(jsonData));

出力結果:

null
99999
123.45
-123.45
1000
0.001
abあいうえおcdef
str2:
\       /ab0ぁ
{ a: 125, b: { c: { d1: [Object], d2: 12345 } } }
{ d1: { v: 1234 }, d2: 12345 }
[ 1, 2, 'test', { a: 5.3 } ]
1
{"null":null,"num1":99999,"num2":123.45,"num3":-123.45,"num4":1000,"num5":0.001,"bool1":true,"bool2":false,"str1":"ab あいうえおcdef","str2":"str2:\n\r\\\t/abc\b0ぁ","obj1":{"a":125,"b":{"c":{"d1":{"v":1234},"d2":12345}}},"ary1":[1,2,"test",{"a":5.3}],"ary2":[1,2,3]}

JSON.parseでJSONからJavaScriptのオブジェクトに、JSON.stringify()でJavaScriptのオブジェクトからJSONに変換できていることが確認できます。
また、num1は重複したデータですが,エラーにはならず、後優先で値が出力されています。

JSONの各値の変換処理

JSON.parseのドキュメントによると引数にreviver が設定できることがわかります。
ここには関数を指定でき,JSONから取得した値を変換することが可能です。
下記の例は数値型をDecimal型に変換するサンプルとなります。

事前準備decimalのインストール


npm install decimal
var fs = require("fs");
// https://www.npmjs.com/package/decimal
var Decimal = require('decimal');
var contents = fs.readFileSync("C:\\share\\jsontest\\test001.json");
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse
var jsonData = JSON.parse(contents, function (key, value) {
    //console.log(key, JSON.stringify(value));
    if (typeof value === 'number') {
      return Decimal(value);
    }
    return value;
  }
);

console.log('-----------------------------------------');

console.log(jsonData['num1'].constructor.name);
console.log(jsonData['num1'].toString());

console.log(jsonData['num2'].constructor.name);
console.log(jsonData['num2'].toString());

console.log(jsonData['obj1']['a'].constructor.name);
console.log(jsonData['obj1']['a'].toString());

出力結果

-----------------------------------------
Decimal
99999
Decimal
123.45
Decimal
125

出力結果より、JSON中のNumber型が全てDecimal型にJavaScriptのオブジェクトとして作成されたことが確認できます。

JSONStream

大きなサイズのJSONファイルを操作する場合、jsonライブラリではメモリを大量に消費し解析に失敗する可能性があります。
この場合は、JSONStreamを使用する必要があります。JSONStreamを使用することで、JSONのデータを一度に全てメモリに展開することなく解析することが可能になります。

インストール


npm install JSONStream
// https://stackoverflow.com/questions/15121584/how-to-parse-a-large-newline-delimited-json-file-by-jsonstream-module-in-node-j
var fs = require('fs'),
    JSONStream = require('JSONStream');

var stream = fs.createReadStream("C:\\share\\jsontest\\test003.json", {encoding: 'utf8'}),
    parser = JSONStream.parse('.');

stream.pipe(parser);

var cnt = 0
parser.on('data', function (obj) {
  // console.log(obj); 
  if (cnt === 0) {
    console.log(obj);
  }
  cnt = cnt + 1;
});

parser.on('end', function () {
  console.log(cnt);
  console.log('end');
});

ファイルストリームを作成してそれをJSONStreamのparserに渡します。
この際、オブジェクトを取得するたびに「data」イベントが発火し、すべてのストリームが完了したら「end」イベントが発火します。
このサンプルでは「data」イベント中で以下の処理をしています。
 ・最初に取得したオブジェクトの内容を表示
 ・dataイベントが発火した数を数える

また「end」イベントでは「data」イベントが発火した数を表示しています。

以下は出力結果になります

C:\dev\node>node jsontest3.js
{ num1: 1, num2: 2 }
6636801
end

詳しい使い方については下記を参照してください。
https://github.com/dominictarr/JSONStream

Python

環境:
Windows10
Python 3.7.4

jsonライブラリ

Pythonの標準ライブラリのjsonライブラリを使用することでJSONの操作を行うことが可能です。

以下のサンプルは「操作対象のJSONの例」を解析するためのプログラムです。

# coding:utf-8
import json

f = open('C:\\share\\jsontest\\test001.json', 'r', encoding="utf-8")

jsonData = json.load(f)
print ('----------------------------------------------------')
print(json.dumps(jsonData, sort_keys = True, indent = 4))
print ('----------------------------------------------------')
print (jsonData['null'])
print (jsonData['num1'])
print (type(jsonData['num1']))
print (jsonData['num2'])
print (type(jsonData['num2']))
print (jsonData['str1'])
print (jsonData['str2'])
print (type(jsonData['obj1']))
print (jsonData['obj1'])
print (jsonData['obj1']['a'])
print (type(jsonData['ary1']))
print (jsonData['ary1'])
print (jsonData['ary1'][0])

f.close()

出力結果は以下のようになります。

C:\dev\python3>python jsontest.py
----------------------------------------------------
{
    "ary1": [
        1,
        2,
        "test",
        {
            "a": 5.3
        }
    ],
    "ary2": [
        1,
        2,
        3
    ],
    "bool1": true,
    "bool2": false,
    "null": null,
    "num1": 99999,
    "num2": 123.45,
    "num3": -123.45,
    "num4": 1000.0,
    "num5": 0.001,
    "obj1": {
        "a": 125,
        "b": {
            "c": {
                "d1": {
                    "v": 1234
                },
                "d2": 12345
            }
        }
    },
    "str1": "ab\u3042\u3044\u3046\u3048\u304acdef",
    "str2": "str2:\n\r\\\t/abc\b0\u3041"
}
----------------------------------------------------
None
99999
<class 'int'>
123.45
<class 'float'>
abあいうえおcdef
str2:
\       /ab0ぁ
<class 'dict'>
{'a': 125, 'b': {'c': {'d1': {'v': 1234}, 'd2': 12345}}}
125
<class 'list'>
[1, 2, 'test', {'a': 5.3}]
1

json.load()でJSONファイルを展開してJSON中のvalueをPythonのデータに下記の表に従って変換します。

JSON Python
object dict
array list
string str
number int または 浮動小数点
true True
false False
null None

出力結果を確認すると上記の表にしたがって型が変換されていることが確認できると思います。
また、元データのJSONには「num1」という名称は2つ存在していますが、出力結果を見ると、後の方のデータが使用されていることが確認できます。

また、json.dumps()はPythonのデータをJSONに変換しています。

jsonライブラリを確認するとjson.loadにはいくつか興味深いパラメータがあります。次節ではそれらを確認してみましょう。

数値の型に対するコールバック関数

json.loadにはparse_floatとparse_intの二つの引数があります。これらの引数にはコールバック関数が設定できます。

以下は、コールバック関数を用いてJSONのnumber型をdecimalに変換した例になります。

# coding:utf-8
import json
import decimal

def callbackInt(d):
  return decimal.Decimal(d)

f = open('C:\\share\\jsontest\\test001.json', 'r', encoding="utf-8")

jsonData = json.load(f, parse_float = decimal.Decimal, parse_int = callbackInt)
print ('--------------------------------------------')
print (jsonData['num1'])
print (type(jsonData['num1']))
print (jsonData['num2'])
print (type(jsonData['num2']))
print (jsonData['num3'])
print (type(jsonData['num3']))

f.close()

出力結果

--------------------------------------------
99999
<class 'decimal.Decimal'>
123.45
<class 'decimal.Decimal'>
-123.45
<class 'decimal.Decimal'>

オブジェクトの型に対するコールバック関数

json.loadにはobject_hookというコールバック関数があります。これはJSON中にobjectを検知するたびに実行される関数です。
数値型に対するコールバック関数と同様に、これを用いてオブジェクトを任意の型に変換することも可能です。

以下の例ではオブジェクト型を検知するたびに、検知した値を出力しています。

# coding:utf-8
import json

def hook_func(dct):
  print (dct)
  return dct

f = open('C:\\share\\jsontest\\test001.json', 'r', encoding="utf-8")

jsonData = json.load(f, object_hook=hook_func)
print ('----------------------------------------------------')

f.close()

出力結果

C:\dev\python3>python jsontest_1.py
{'v': 1234}
{'d1': {'v': 1234}, 'd2': 12345}
{'c': {'d1': {'v': 1234}, 'd2': 12345}}
{'a': 125, 'b': {'c': {'d1': {'v': 1234}, 'd2': 12345}}}
{'a': 5.3}
{'null': None, 'num1': 99999, 'num2': 123.45, 'num3': -123.45, 'num4': 1000.0, 'num5': 0.001, 'bool1': True, 'bool2': False, 'str1': 'abあいうえおcdef', 'str2': 'str2:\n\r\\\t/abc\x080ぁ', 'obj1': {'a': 125, 'b': {'c': {'d1': {'v': 1234}, 'd2': 12345}}}, 'ary1': [1, 2, 'test', {'a': 5.3}], 'ary2': [1, 2, 3]}
----------------------------------------------------

C:\dev\python3>

出力結果を見てわかる通り、基本的に上から見ていきますが、オブジェクトの中にオブジェクトがある場合は、もっとも深い箇所から検知されていきます。
任意のオブジェクトに変換する場合は、コールバック関数の引数であるdictの全てのキーをみて判断するか、任意の項目に変換すべき型の情報を記載しておく必要があるでしょう。

オブジェクトや配列を含まないデータの場合

最近のJSONの仕様ではオブジェクトや配列を含まない場合もJSONをパースできます。
これは以下のようなコードで検証できます。

import json
json.loads('12345')
# 12345

jsonライブラリにおいては、この仕様を満たしています。

NaNやInfinityなどの数値じゃない値を含む場合

import json
n = json.loads('NaN')
print(n)
# nan
json.dumps(n)
# 'NaN'

NaNやInfinityを含む場合、デフォルトload/loadsの挙動ではnaninfに変換されます。
また、nanやinfを含むPythonの値をdump/dumpsでJSON化した場合、NaNやInfinityに変換されます。

もし、仕様と合わせてJSON中のNaNやInfinityの値をエラーとする場合は以下のようにします。

def errorfunc(d):
   print(d)
   raise

n = json.loads('NaN', parse_constant=errorfunc)

これを実行すると、NaNなどを検知した場合にerrorfuncが実行されて例外が発生します。
また、逆にPythonのデータをJSON化する場合にNaNやInfinityを認めない場合は以下のような実装になります。

n = json.loads('NaN')
json.dumps(n, allow_nan=False)

この場合、dumps実行時にnanやinfを検出すると「ValueError: Out of range float values are not JSON compliant」が発生します。

ijsonライブラリ

大きなサイズのJSONファイルを操作する場合、jsonライブラリではメモリを大量に消費し解析に失敗する可能性があります。
この場合は、ijsonを使用します。

ijsonはイテレータのインターフェイスを経由してJSONを解析することにより、すべてのJSONデータをメモリに展開する必要がなくなります。
以下のサンプルコードは大量データのJSONを解析している例です。

# coding:utf-8
import ijson

def test1():
  # 数百メガのファイルを開く
  f = open('C:\\share\\jsontest\\test003.json', 'r', encoding="utf-8")
  items = ijson.items(f, 'item')
  l = 0
  for i in items:
    if l == 0:
      print (i)  # 最初のオブジェクトのみ表示
    l = l + 1
  print (l)
  f.close()

if __name__ == "__main__":
  test1()

ijson.itemsにfile object と prefixを与えて抽出を行います。prefixを設定することで、特定の要素のみを抽出対象にすることができます。今回はarrayの各要素を取得したいので「item」を指定してます。

このファイルの実行結果としては以下のように、最初のオブジェクトと、オブジェクトの総数を出力します。

C:\dev\python3>python jsontest3_3.py
{'num1': 1, 'num2': 2}
6636801

ijson
https://pypi.org/project/ijson/

ijson - Github
https://github.com/isagalaev/ijson

C#

環境:
Windows10
VisualStudio2019
.NET 4.7.2

Json.NET

NewtonSoftが提供するJson.NETを持ちいることでJSONに対する操作を行います。
https://www.newtonsoft.com/json

ASP.NETで開発した人は、おそらく良く見るライブラリかと思います。

NuGetで以下のパッケージをインストールしてください。
image.png

型を指定しない使用方法

JsonConvert.DeserializeObjectに型を指定しないでJSONデータを読み込むことができます。これにより、あらかじめJSONの形に合わせたクラスを用意する必要がなくなります。

        static void Test1()
        {
            var contents = File.ReadAllText(@"c:\share\jsontest\test001.json");
            dynamic json = JsonConvert.DeserializeObject(contents);
            Console.WriteLine(json.num1.Type);
            Console.WriteLine(json.num1.Value);

            Console.WriteLine(json.num2.Type);
            Console.WriteLine(json.num2.Value);

            Console.WriteLine(json.num3.Type);
            Console.WriteLine(json.num3.Value);

            Console.WriteLine(json.num4.Type);
            Console.WriteLine(json.num4.Value);

            Console.WriteLine(json.num5.Type);
            Console.WriteLine(json.num5.Value);

            Console.WriteLine(json["null"].Type);
            Console.WriteLine(json["null"].Value == null);

            Console.WriteLine(json.bool1.Type);
            Console.WriteLine(json.bool1.Value);

            Console.WriteLine(json.bool2.Type);
            Console.WriteLine(json.bool2.Value);

            Console.WriteLine(json.str1.Type);
            Console.WriteLine(json.str1.Value);

            Console.WriteLine(json.str2.Type);
            Console.WriteLine(json.str2.Value);

            Console.WriteLine(json.obj1.a.Type);
            Console.WriteLine(json.obj1.a.Value);

            Console.WriteLine(json.ary1.Count);
            Console.WriteLine(json.ary1[0].Type);
            Console.WriteLine(json.ary1[0].Value);

        }

出力結果は以下のようになります。

Integer
99999
Float
123.45
Float
-123.45
Float
1000
Float
0.001
Null
True
Boolean
True
Boolean
False
String
abあいうえおcdef
String
str2:
\       /ab0ぁ
Integer
125
4
Integer
1

重複している値num1が、後優先で取得されていることが確認できます。

型を指定する方法

JsonConvert.DeserializeObjectに型を指定して呼び出すことで、JSONの解析結果を事前に用意したクラスに割り当てることも可能です。

使用するJSONデータ

[
  {"name":"Joe", "lv" : 10},
  {"name":"Jack", "lv" : 15},
  {"name":"Sara", "lv" : 13},
  {"name":"Ben", "lv" : 12}
]
       class Member {
            public string name { get; set; }
            public decimal lv { get; set; }

        }

        static void Test2()
        {
            var contents = File.ReadAllText(@"c:\share\jsontest\test004.json");
            List<Member> json = JsonConvert.DeserializeObject<List<Member>>(contents);
            foreach (var m in json)
            {
                Console.WriteLine(m.name );
                Console.WriteLine(m.lv);
            }

            Console.WriteLine("------------------------------------------");
            Console.WriteLine(JsonConvert.SerializeObject(json, Formatting.Indented));

        }

このサンプルではJSONをList\<Member>に明示的に変換することを行っています。
また、 List\<Member>をJsonConvert.SerializeObjectを使用してJSONに戻しています。

出力結果:

Joe
10
Jack
15
Sara
13
Ben
12
------------------------------------------
[
  {
    "name": "Joe",
    "lv": 10.0
  },
  {
    "name": "Jack",
    "lv": 15.0
  },
  {
    "name": "Sara",
    "lv": 13.0
  },
  {
    "name": "Ben",
    "lv": 12.0
  }
]

出力結果が10.0,15.0などの小数点表記になっているのはMemberクラスのlvがdecimalのためです。

大量のデータを解析する方法

大量のデータを少ないメモリで処理するにはJsonTextReaderを使用します。

        static void Test3()
        {
            using (FileStream fs = new FileStream(@"c:\share\jsontest\test003.json", FileMode.Open, FileAccess.Read))
            using (StreamReader sr = new StreamReader(fs))
            using (JsonTextReader reader = new JsonTextReader(sr))
            {
                int cnt = 0;
                while (reader.Read())
                {
                    if (reader.TokenType == JsonToken.StartObject)
                    {
                        // Load each object from the stream and do something with it
                        JObject obj = JObject.Load(reader);
                        //Console.WriteLine(obj["num1"]);
                        ++cnt;
                    }
                }
                Console.WriteLine(cnt);
            }
        }

ファイルストリームをJsonTextReaderに渡し、StartObjectを検知する度に処理を行うことで、すべてのデータをメモリに展開せずに解析が可能になります。

RFCより緩い挙動

RFC7159で認められていないJSONをJSONとしてみなす挙動が何点かあります。
https://github.com/JamesNK/Newtonsoft.Json/issues/646

たとえば以下のコードはjavascriptのJSON.parseでは例外となりますが、Json.NETでは通ってしまいます。

           // 二重引用符ではなく一重引用符を含むプロパティ
            dynamic json =  JsonConvert.DeserializeObject("{'a':1}");
            Console.WriteLine(json);
            // 引用符なしのプロパティ
            json = JsonConvert.DeserializeObject("{a:1}");
            Console.WriteLine(json);
            // NaN,Infinity, -Infinity
            json = JsonConvert.DeserializeObject("{\"a\":NaN}");
            Console.WriteLine(json.a);
            // 末尾のコンマ
            json = JsonConvert.DeserializeObject("{\"a\": 123,}");
            Console.WriteLine(json);
            // 空のコンマ
            json = JsonConvert.DeserializeObject("[1,,2]");
            Console.WriteLine(json);
            // 8進数
            json = JsonConvert.DeserializeObject("{\"a\": 010}");
            Console.WriteLine(json);
            // 16進数
            json = JsonConvert.DeserializeObject("{\"a\": 0x010}");
            Console.WriteLine(json);
{
  "a": 1
}
{
  "a": 1
}
NaN
{
  "a": 123
}
[
  1,
  null,
  2
]
{
  "a": 8
}
{
  "a": 16
}

2018年時点の仕様より多くのものを許可してしまっているということに注意しなければいけません。特に複数のプログラミング言語を使用している場合、注意が必要です。

例:C#で通ったJSONがnode.jsとかでは通らないとかがあり得る。

PowerShell

環境:
Windows10
PSVersion 5.1.17134.765
PSEdition Desktop

ConvertFrom-Json と ConvertTo-Json

ConvertFrom-JsonはJSONをPowerShellのオブジェクトに変換し、ConvertTo-JsonはPowerShellのオブジェクトをJSONに変換します。

ConvertFrom-Json
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/convertfrom-json?view=powershell-5.1

ConvertTo-Json
https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/convertto-json?view=powershell-5.1

JavaScriptSerializer classを用いて実装しているとのことですが、これはC#で出てきたJson.NET を使用しているようです。

$json = Get-Content  -Encoding UTF8 c:\share\jsontest\test001.json  | ConvertFrom-Json
Write-Host $json.GetType()
Write-Host $json
Write-Host $json.num1.GetType()
Write-Host $json.num1
Write-Host $json.num2.GetType()
Write-Host $json.num2
Write-Host $json.num3.GetType()
Write-Host $json.num3
Write-Host $json.num4.GetType()
Write-Host $json.num4
Write-Host $json.num5.GetType()
Write-Host $json.num5
Write-Host $json.obj1.GetType()
Write-Host $json.obj1
Write-Host $json.obj1.a
Write-Host $json.obj1.b.GetType()
Write-Host $json.obj1.b
Write-Host $json.obj1.b.c.GetType()
Write-Host $json.obj1.b.c
Write-Host $json.obj1.b.c.d1.GetType()
Write-Host $json.obj1.b.c.d1
Write-Host $json.ary1.GetType()
Write-Host $json.ary1
Write-Host $json.ary1[0]

# https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/convertto-json?view=powershell-5.1
$str = ConvertTo-Json -depth 100 -InputObject $json
Write-Host $str

実行結果は以下のようになります。

System.Management.Automation.PSCustomObject
@{null=; num1=99999; num2=123.45; num3=-123.45; num4=1000; num5=0.001; bool1=True; bool2=False; str1=abあいうえおcdef; str2=str2:
\       /ab0ぁ; obj1=; ary1=System.Object[]; ary2=System.Object[]}
System.Int32
99999
System.Decimal
123.45
System.Decimal
-123.45
System.Double
1000
System.Double
0.001
System.Management.Automation.PSCustomObject
@{a=125; b=}
125
System.Management.Automation.PSCustomObject
@{c=}
System.Management.Automation.PSCustomObject
@{d1=; d2=12345}
System.Management.Automation.PSCustomObject
@{v=1234}
System.Object[]
1 2 test @{a=5.3}
1
{
    "null":  null,
    "num1":  99999,
    "num2":  123.45,
    "num3":  -123.45,
    "num4":  1000,
    "num5":  0.001,
    "bool1":  true,
    "bool2":  false,
    "str1":  "abあいうえおcdef",
    "str2":  "str2:\n\r\\\t/abc\b0ぁ",
    "obj1":  {
                 "a":  125,
                 "b":  {
                           "c":  {
                                     "d1":  {
                                                "v":  1234
                                            },
                                     "d2":  12345
                                 }
                       }
             },
    "ary1":  [
                 1,
                 2,
                 "test",
                 {
                     "a":  5.3
                 }
             ],
    "ary2":  [
                 1,
                 2,
                 3
             ]
}

重複しているnum1は後方が優先されており、JSONのObject型はSystem.Management.Automation.PSCustomObject、JSONのArray型はSystem.Object[]に変換されています。
JSONのNumber型はInt32,Double,Decimalのいずれかの型に変換されていることが確認でき、型の取り扱いに注意が必要と考えられます。

Excel VBA

環境:
Windows10
Office16(32bit)

JScriptを用いた変換

UTF8のJSONファイルをADODB.Streamで読み込み、それをJScriptのオブジェクトに変換する方法が、下記のページに紹介されていました。

How to parse JSON with VBA without external libraries?
https://stackoverflow.com/questions/19360440/how-to-parse-json-with-vba-without-external-libraries

この方法は正常に動作しないケースがあります。

Private Function ReadUtf8File(ByVal path As String) As String
    Dim objStream, strData

    Set objStream = CreateObject("ADODB.Stream")

    objStream.Charset = "utf-8"
    objStream.Open
    Call objStream.LoadFromFile(path)

    ReadUtf8File = objStream.ReadText()

    objStream.Close
    Set objStream = Nothing
End Function

Sub Test1_1()
    ' https://stackoverflow.com/questions/19360440/how-to-parse-json-with-vba-without-external-libraries
    Dim json As Object
    Dim scriptControl As Object

    Set scriptControl = CreateObject("MSScriptControl.ScriptControl")
    scriptControl.Language = "JScript"

    Dim contents As String
    contents = ReadUtf8File("C:\\share\\jsontest\\test001.json")

    Set json = scriptControl.Eval("(" + contents + ")")
    Debug.Print json

    ' 配列が操作できない

End Sub

配列の操作ができない
image.png

ウォッチ式を見る限り配列として取得されているようだが、配列の操作が一切できない。

array, objectを含まないJSONが解析できない

Sub Test1_2()
    Dim json As Object
    Dim scriptControl As Object

    Set scriptControl = CreateObject("MSScriptControl.ScriptControl")
    scriptControl.Language = "JScript"

    Dim contents As String
    contents = "12345"

    ' エラー
    Set json = scriptControl.Eval("(" + contents + ")")
End Sub

VBA-JSON

VBA-JSONはVBAでJSONの変換を可能としたライブラリです。
https://github.com/VBA-tools/VBA-JSON

使用方法としては以下の通りです。
1. 「Microsoft Scripting Runtime」を参照設定すること
2. JsonConverter.basを取り込む
3. 下記のような実装を行う

Sub test2_1()
    Dim contents As String
    contents = ReadUtf8File("C:\\share\\jsontest\\test001.json")
    Dim Parsed As Dictionary
    Set Parsed = JsonConverter.ParseJson(contents)
    Debug.Print Parsed.Item("num1")
    Debug.Print Parsed.Item("num2")
    Debug.Print Parsed.Item("null")
    Debug.Print Parsed.Item("obj1").Item("a")
    Debug.Print Parsed.Item("ary1").Count
    Debug.Print Parsed.Item("ary1").Item(1)
    Debug.Print Parsed.Item("ary1").Item(2)
    Debug.Print Parsed.Item("ary1").Item(3)
End Sub

先ほどと違い配列データも正常に取得が可能になっています。

Test2_1
 99999 
 123.45 
Null
 125 
 4 
 1 
 2 
test

ただし、最新の仕様に対応していないため、以下のようにobjectまたはarrayを含まないJSONについて仕様通り動作しません。

Sub test2_2()
    Dim contents As String
    contents = "12345"
    Dim Parsed As Dictionary
    ' エラー
    Set Parsed = JsonConverter.ParseJson(contents)
End Sub

さいごに

今回は謙虚な気持ちでJSONについて調べなおしてみました。

わかってたけど なにもわかっちゃいなかったことも まれによくあるので、わかったつもりになっているモノこそ基礎から見直してみることも必要かもしれません。

さもないと慢心の精霊に取りつかれて「JSONはobjectまたはarrayを必ず含む必要ある!どや!( ・`ー・´)」とか言って老害っぷりをさらすことになります。

その他・参考

Wikipedia
https://ja.wikipedia.org/wiki/JavaScript_Object_Notation
https://en.wikipedia.org/wiki/JSON

how to parse a large, Newline-delimited JSON file by JSONStream module in node.js?
https://stackoverflow.com/questions/15121584/how-to-parse-a-large-newline-delimited-json-file-by-jsonstream-module-in-node-j

Json.NETドキュメント
https://www.newtonsoft.com/json/help/html/Introduction.htm

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です