LoginSignup
26
23

More than 3 years have passed since last update.

[Delphi] JSON の処理は Serializer を使うのがナウい

Last updated at Posted at 2020-05-05

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 から記事をランダムで取得した場合を見てみます。

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 なら超簡単なので、オススメです。
ただし、高機能になればなるほど処理は重くなる事は忘れないようにしてください。

26
23
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
26
23