JSON
サーバー・クライアント間でデータをやり取りするフォーマットとして XML と JSON が有名所だと思います。
それぞれのフォーマットについて詳しい所はググればいくらでも出てくるので、ここでは述べません。
ただ、XML に比べて JSON が勝っているところを上げておくと…
- XML よりサイズが小さくなる場合が多い
- Delphi RTL や .NET といった一部のフレームワークには JSON Serializer が用意されている
ということです。
最近、僕が自分でフォーマットを決められる場合は、ほぼ必ず JSON を指定しています。
なぜなら JSON Serializer が便利すぎるから!
…で、さっきはググればいくらでも出てくるから述べないと書きましたが、JSON Serializer も同じで色々引っかかります。
では、何故書くのかというと、Qiita には、Delphi の TJsonSerializer の記事がないからです!
(正確には "クソアプリを作った件" で簡単に紹介していますが)
Delphi で JSON を扱う
実は Delphi には JSON を扱う仕組みが2つ載っています。
それぞれについては上記のリンクをご覧ください。
DocWiki 上では、この2つしか紹介がありませんが、3つめの仕組みとして JsonSerializer があるのです。
TJsonSerializer の使い方
未だに DocWiki には TJsonSerializer の説明がないため、機能を知らない方も多いと思います。
TJsonSerializer を一言で纏めると、JSON を構造体・クラスとして読み書きできる仕組み、です。
例1
例えば↓のような JSON があるとします。
{
"foo":"30",
"bar":"Sample",
"baz":["Hoge", "Fuga"]
}
では、これを旧来の JSON オブジェクトフレームワークを使った書き方で読んでみると…
// uses System.JSON が必要
procedure ReadJsonByJsonObject;
begin
var Sample := TJsonObject.ParseJSONValue(SAMPLE); // SAMPLE は上記の JSON
try
// JSON の内容を読み取る
var foo := Sample.GetValue<Integer>('Foo');
var bar := Sample.GetValue<String>('Bar');
var baz := Sample.GetValue<TJsonArray>('Baz');
// 出力
Writeln(foo);
Writeln(bar);
for var V in baz do
Writeln(V.GetValue<String>);
finally
Sample.DisposeOf;
end;
end;
こんな感じです。
次に TJsonSerializer を使うと…
// uses System.JSON.Serializers が必要
procedure ReadJsonBySerializer;
type
// JSON を構造体として定義する
// Field 名は、JSON の Key 名と同じにする
TSample = record
foo: Integer;
bar: String;
baz: TArray<String>;
end;
begin
var S := TJsonSerializer.Create;
try
// JSON の内容を読み取る
var Sample := S.Deserialize<TSample>(SAMPLE);
// 出力
Writeln(Sample.foo);
Writeln(Sample.bar);
for var V in Sample.baz do
Writeln(V);
finally
S.DisposeOf;
end;
end;
構造体が定義されているので、どんな内容の JSON が来るのか一目瞭然ですし、一々型を指定して取り出す必要もありません!
TJsonObject を使った方法は、出力時にも TJsonObject の機能を使います( V.GetValue<String>
の部分)。
読み込んだデータを他の処理に渡すとき、必然的に他の処理も TJsonObject を使って処理しなければなりません。
一方、TJsonSerializer は、出力時は JSON の機能は使わず、Delphi の基本型を使っているだけです。
つまり、読み込んだデータを別の処理に渡すとき、別の処理は JSON の事を知らなくていいのです。
いまのサンプルは簡単だったので、TJsonObject を使ってもそんなに大変じゃないんじゃない?とお思いでしょう。
しかし、複雑な JSON の場合どうでしょう?
例2
クソアプリを作った件と同じ MediaWiki API で Wikipedia から記事をランダムで取得した場合を見てみます。
https://ja.wikipedia.org/w/api.php?format=json&action=query&rnnamespace=0&list=random
すると、こんな JSON が戻ってきます。
{
"batchcomplete": "",
"continue": {
"rncontinue": "0.833062629913|0.833063470494|2568882|0",
"continue": "-||"
},
"query": {
"random": [
{
"id": 2558363,
"ns": 0,
"title": "Predia"
}
]
}
}
これを TJsonObject で書くと
procedure ReadJsonByJsonObject;
type
TRandom = record
Id: Integer;
Ns: Integer;
Title: String;
end;
begin
var Root := TJsonObject.ParseJSONValue(MEDIA_WIKI);
try
// 読み取り
var Batchcomplete := Root.GetValue<String>('batchcomplete');
var Cont := Root.GetValue<TJsonObject>('continue');
var Cont_RNContinue := Cont.GetValue<String>('rncontinue');
var Cont_Continue := Cont.GetValue<String>('continue');
var Q := Root.GetValue<TJsonObject>('query');
var Q_Rand := Q.GetValue<TJsonArray>('random');
var Q_Rands: TArray<TRandom>;
SetLength(Q_Rands, Q_Rand.Count);
var i := 0;
for var V in Q_Rand do
begin
Q_Rands[i].Id := V.GetValue<Integer>('id');
Q_Rands[i].Ns := V.GetValue<Integer>('ns');
Q_Rands[i].Title := V.GetValue<String>('title');
Inc(i);
end;
// 出力部は省略
finally
Root.DisposeOf;
end;
end;
TJsonSerializer だと
procedure ReadJsonBySerializer;
type
TRandom = record // random 配列の要素はオブジェクト
id: Integer;
ns: Integer;
title: String;
end;
TQuery = record // query もオブジェクト
random: TArray<TRandom>; // 上記の TRandom を要素として持つ配列
end;
TContinue = record // continue もオブジェクト
rncontinue: String;
continue: String;
end;
TRoot = record // JSON 全体の定義
batchcomplete: String;
continue: TContinue; // continue は TContinue として
query: TQuery; // query は TQuery として読み込む
end;
begin
var S := TJsonSerializer.Create;
try
// 読み取り
var Root := S.Deserialize<TRoot>(MEDIA_WIKI);
// 出力部は省略
finally
S.DisposeOf;
end;
end;
どちらがエレガントか、一目瞭然ではありませんか!?
TJsonSerializer を使った方法は、どんなに複雑な JSON でも読込はたったの1行です。
複雑さが増せば増すほど、JsonSerializer を使った方が可読性が高くなります。
TJsonSerializer を使った JSON の生成
次に JSON を生成する方法を見てみます。
ここからは TJsonSerializer のサンプルのみです。
下記の JSON で考えてみます。
{
"name":"御坂美琴",
"age":"14",
"nickname":["常盤台のレールガン", "Level5 の No.3"]
"ability": {"name":"電撃使い", "Level":"5"}
}
まずこの JSON を表す構造体を定義します。
type
TAbility = record // ability が指す JSON Object の構造体を定義して
name: String;
level: Integer;
end;
TPerson = record // JSON の全体を指す構造体を定義
name: String;
age: Integer;
nickname: TArray<String>;
ability: TAbility; // ability は TAbility 構造体
end;
こんな感じですね。
ability は {} で括られるためオブジェクトです。
ですので ability は TAbility という構造体で定義しました。
あとは、下記の様に値をセットして
// 構造体に値をセット
var Person: TPerson ;
Person.name := '御坂美琴';
Person.age := 14;
Person.nickname := ['常盤台のレールガン', 'Level 5 の No.3'];
Person.ability.name := '電撃使い';
Person.ability.Level := 5;
TJsonSerializer.Serialize を呼ぶだけで JSON が生成されます。
var S := TJsonSerializer.Create;
try
// TJsonFormatting.Indent を指定するとインデントした JSON を出力する
S.Formatting := TJsonFormatting.Indented;
// JSON 化はたったの1行
var Json := S.Serialize<TPerson>(Person);
// 出力
Writeln(Json);
finally
S.DisposeOf;
end;
{
"name": "御坂美琴",
"age": 14,
"nickname": [
"常盤台のレールガン",
"Level 5 の No.3"
],
"ability": {
"name": "電撃使い",
"level": 5
}
}
どんなに複雑になっても、やり方は同じです。
でもここで「Object Pascal の予約語が JSON のキー名と同じだったらどうするのさ!」という疑問が!
TJsonSerializer の属性
TJsonSerializer に使える属性が System.JSON.Serializers に定義されています。
先ほどの疑問を解決するのが JsonName 属性です。
JsonName 属性
JsonName 属性は、Delphi の Field 名と JSON の Key 名が異なっている場合に威力を発揮します。
例えば
{"begin":"はじめ","end":"おわり"}
という JSON だと begin や end は Object Pascal の予約語なのでキー名には使えません。
このような時に JsonName 属性を使うと
TSample = record.
[JsonName('begin')]
_begin: String;
[JsonName('end')]
_end: String;
end;
こんな風に書けます。
つまり、Field 名を Key 名として使うのでは無く JsonName 属性で Key 名を指定する、ということです。
JsonIgnore 属性
JsonIgnore は JSON を生成する際に無視するキーを指定します。
例えば、最初の JSON では foo, bar, baz が存在しましたが、この構造体に FTag という識別用のタグを付けたいとします。
TSample = record.
foo: Integer;
bar: String;
baz: TArray<String>;
FTag: Integer;
end;
これをそのまま TJsonSerializer.Serialize すると
{"foo":100,"bar":"aaa","baz":["A","B","C"],"FTag":0}
こんな風に FTag も JSON に含まれてしまいます
そこで、FTag に JsonIgnore 属性を付けてあげると…
TSample = record.
foo: Integer;
bar: String;
baz: TArray<String>;
[JsonIgnore]
FTag: Integer;
end;
↓このように FTag が含まれなくなります。
{"foo":100,"bar":"aaa","baz":["A","B","C"]}
JsonConverter 属性
TJsonSerailizer は、多くの型にデフォルトで対応していますが、自分で定義した型だったり、普通とは違う方法で値を処理したい場合などに力を発揮します。
ちょっとあまり良い例ではないのですが、例えば
{"foo":{"a":"aaa", "b":"bbb"}}
{"foo":{"n1":"111", "n2":"222"}}
↑こんな風に foo の中身が変わる時、a, b, n1, n2 という4つのキーを持つクラスを作り、使う側で判断してもいいのですが、もっと複雑になった時面倒です。読み取った時点で判断してみます。
まず、foo のために a, b, n1, n2 という4つのフィールドを持つ TFoo 型を定義します。
そして、どちらのタイプの JSON なのかを見分けるフラグ DataType を定義します。
なお、この時注意したいのが JsonSerializer で生成されたクラスは自動的に破棄されるのですが、このクラスは自分で生成するので自動的に破棄されるように TInterfacedObject を利用します。
type
// 自動開放されるように Interfaced Object を継承する
TFoo = class(TInterfacedObject)
public type
TDataType = (Text, Number);
public
a, b: String;
n1, n2: Integer;
[JsonIgnore]
DataType: TDataType; // a, b が有効な時は Text, n1, n2 が有効な時は Number を返す
end;
といった型を定義して、さらに JsonConverter を定義し
// TJsonConverter の定義には下記の Unit 全てが必要
uses
System.SysUtils,
System.TypInfo,
System.Rtti,
System.JSON,
System.JSON.Types,
System.JSON.Serializers,
System.JSON.Readers,
System.JSON.Writers;
type
TFooConverter = class(TJsonConverter)
public
// 該当キーが読まれるときに呼ばれる, 今回は省略したが WriteJson もある
function ReadJson(
const AReader: TJsonReader;
ATypeInf: PTypeInfo;
const AExistingValue: TValue;
const ASerializer: TJsonSerializer): TValue; override;
end;
function TFooConverter.ReadJson(
const AReader: TJsonReader;
ATypeInf: PTypeInfo;
const AExistingValue: TValue;
const ASerializer: TJsonSerializer): TValue;
begin
Result := TValue.Empty;
// Json が null なら何もしない
if AReader.TokenType = TJsonToken.Null then
Exit;
// Foo を生成して、戻り値を設定
var Foo := TFoo.Create;
Result := TValue.From(Foo);
// AReader から JSON を読み出して、Foo に設定する
ASerializer.Populate(AReader, Foo);
// a が空の時は Number とする
if Foo.a.IsEmpty then
Foo.DataType := TFoo.TDataType.Number
else
Foo.DataType := TFoo.TDataType.Text;
end;
この TFooConverter を foo の JsonConveter に指定します。
type
TSample = class
[JsonConverter(TFooConverter)]
foo: TFoo;
end;
このように定義すると、foo を読み込むときに自動的に TFooConverter が生成され実行されます。
すると、こんな風に foo の中身に応じて DataType が設定されるので、利用側で即座に判断できる、というわけです。
procedure ReadData;
const
JSON_TEXT = '{"foo":{"a":"aaa", "b":"bbb"}}';
JSON_NUM = '{"foo":{"n1":"111", "n2":"222"}}';
begin
var S := TJsonSerializer.Create;
try
var Data1 := S.Deserialize<TSample>(JSON_TEXT);
if Data1.foo.DataType = TFoo.TDataType.Text then // Text?
Writeln('Text: ', Data1.foo.a, ', ', Data1.foo.b);
var Data2 := S.Deserialize<TSample>(JSON_NUM);
if Data2.foo.DataType = TFoo.TDataType.Number then // Number?
Writeln('Number: ', Data2.foo.n1, ', ', Data2.foo.n2);
finally
S.DisposeOf;
end;
end;
つまり、上記の例のように JsonConverter を作成すれば JSON のデータ解析時に独自の処理を組み込めます。
その他の属性
JsonSerialize
Serialize/Deserialize するメンバーを指定します。
次の TJsonMemberSerialization 型の3つの値が指定できます
指定 | Serialize 対象 |
---|---|
Fields | フィールド |
Public | Public なメンバー |
In | JsonIn で指定されている物 |
JsonIn
上記の JsonSerialize で TJsonMemberSerialization.In が指定されている時、Serialize/Deserialize 対象になります。
JsonObjectHandling
JsonObjectOwnership
Object の生成方法やそのインスタンスの扱いを決定する属性です。
とてつもなく大きなクラスではない限り、Default のままで特に問題無いので、ここでは割愛します。
構造体とクラス
JsonConveter の所でサラッと構造体では無くクラスを使いましたが、基本的にはどちらもで構いません。
ただ、Json の値として nil がある場合は、class の方が直感的でしょう(構造体だとメモリそのものは存在してしまうため、キーが存在しないのか、キーの中身が空なのかどうか判別できない)
まとめ
JSON を TJsonObject で使ってると死にそうになるけど、TJsonSerializer なら超簡単なので、オススメです。
ただし、高機能になればなるほど処理は重くなる事は忘れないようにしてください。