2018年5月19日土曜日

TokyoでDictionaryを要素にもつJson形式のファイルの読み書きをしてみた。

前回、LIST構造を持つJSON形式のファイルを読み書きしたので、今回はDictionary構造を持つファイルを読み書きしてみた。
(プロジェクトは、https://bitbucket.org/OldTPFun/delphitest/src/master/JsonTest/Proj2/ に配置してあります。)

今回読み書きするのは以下のようなファイル。

{"TownName":"木組みの家と石畳の街",
    "Shops":{
        "ラビットハウス":
            {"ShopName":"ラビットハウス",
                "Clerks":[
                    {"Name":"ココア","Age":17},
                    {"Name":"チノ","Age":15},
                    {"Name":"リゼ","Age":17}
                ]      
            },
        "甘兎庵":
            {"ShopName":"甘兎庵",
                "Clerks":[
                    {"Name":"チヤ","Age":17}
                ]
            }
    }
}


先ずは書き込みから。

今回は、Keyが文字列で、ValueがTCoffeeShop型のTDictionaryをシリアライズ・デシリアライズすればよいので、前回にならって、TDictionary用のコンバータ

  TCoffieShopDicConverter = class(TJsonDictionaryConverter<string, TCoffeeShop>);

を作成して、上記のJSON文字列にシリアライズするためのクラスを作成して、TDictionary型の変数にTCoffieShopDicConverter属性を設定

  [JsonSerialize(TJsonMemberSerialization.&Public)]
  TCofeeShopList = class
  private
    FTownName: String;
    FShops: TObjectDictionary<string, TCoffeeShop>;
    procedure SetTownName(const Value: String);
    procedure SetShops(const Value: TObjectDictionary<string, TCoffeeShop>);
    public
      property TownName : String read FTownName write SetTownName;
      [JsonConverter(TCoffieShopDicConverter)]
      property Shops : TObjectDictionary<string, TCoffeeShop> read FShops write SetShops;
      constructor Create;
  end;

で、シリアライズするためのコード

procedure TForm1.Button1Click(Sender: TObject);
var
  CoffeeShop1,CoffeeShop2 : TCoffeeShop;
  serializer: TJsonSerializer;
  CofeeShopList : TCofeeShopList;
  s : string;
begin
  CofeeShopList := TCofeeShopList.Create;
  CoffeeShop1 := TCoffeeShop.Create;
  CoffeeShop2 := TCoffeeShop.Create;
  try
    CoffeeShop1.ShopName := 'ラビットハウス';
    CoffeeShop1.Clerks.Add(TClerk.Create('ココア',17));
    CoffeeShop1.Clerks.Add(TClerk.Create('チノ',15));
    CoffeeShop1.Clerks.Add(TClerk.Create('リゼ',17));

    CoffeeShop2.ShopName := '甘兎庵';
    CoffeeShop2.Clerks.Add(TClerk.Create('チヤ',17));

    CofeeShopList.TownName := '木組みの家と石畳の街';
    CofeeShopList.Shops.Add(CoffeeShop1.ShopName,CoffeeShop1);
    CofeeShopList.Shops.Add(CoffeeShop2.ShopName,CoffeeShop2);

    serializer := TJsonSerializer.Create;
    try

      s := serializer.Serialize(CofeeShopList);
      Memo1.Text := s;

      TFile.WriteAllText('CoffeeShop.Json',s);

    finally
      serializer.Free;
    end;

  finally
    CofeeShopList.Shops.Clear;
    CofeeShopList.Free;
  end;
end;

を書いて実行すると、見事に成功と思いきや・・・・

例外クラスEAbstractError (メッセージ'抽象エラー')を送出しました。

と例外が発生。ありゃりゃ。

デバッグ実行した結果、PropertyToKeyの呼び出し時に例外が発生していることが分かったので、System.JSON.Converters.pasのTJsonDictionaryConverter<k,v>の定義を調べてみると、
TJsonDictionaryConverter<k,v> = class(TJsonConverter)
  protected
    function CreateDictionary: TDictionary<k,v>; virtual;
    function PropertyToKey(const APropertyName: string): K; virtual; abstract;
    function KeyToProperty(const AKey: K): string; virtual; abstract;
    function ReadKey(const AReader: TJsonReader; const ASerializer: TJsonSerializer): string;
    function ReadValue(const AReader: TJsonReader; const ASerializer: TJsonSerializer): V; virtual;
    procedure WriteValue(const AWriter: TJsonWriter; const AValue: V; const ASerializer: TJsonSerializer); virtual;
  public
    procedure WriteJson(const AWriter: TJsonWriter; const AValue: TValue; const ASerializer: TJsonSerializer); override;
    function ReadJson(const AReader: TJsonReader; ATypeInf: PTypeInfo; const AExistingValue: TValue;
      const ASerializer: TJsonSerializer): TValue; override;
    function CanConvert(ATypeInf: PTypeInfo): Boolean; override;
  end;

と、PropertyToKey,とKeyToPropertyにabstractがついているので、この2つは、継承先のコンバーターで実装しないといけなかったのね。

System.JSON.Converters.pasに、TJsonDictionaryConverterを継承した、キーが文字列のTJsonStringDictionaryConverter<V>の定義が あるので、中身をみてみると、

  TJsonStringDictionaryConverter<V> = class(TJsonDictionaryConverter<string, V>)
  protected
    function PropertyToKey(const APropertyName: string): string; override;
    function KeyToProperty(const AKey: string): string; override;
  end;

とあって、実装が

function TJsonStringDictionaryConverter<V>.KeyToProperty(const AKey: string): string;
begin
  Result := AKey;
end;

function TJsonStringDictionaryConverter<V>.PropertyToKey(const APropertyName: string): string;
begin
  Result := APropertyName;
end;

となっているので、Dictionaryのコンバーターを作成する時は、TJsonDictionaryConverterから、キーの型を決めた派生型を作成し、KeyToPropertyにはキーから文字列に変換、PropertyToKeyには文字列からキーの型のインスタンスへの変換を自前で実装すれば、良いわけですね。
例えば、キーが整数型のDictionaryのコンバータは

  TJsonIntegerDictionaryConverter<V> = class(TJsonDictionaryConverter<Integer, V>)
  protected
    function PropertyToKey(const APropertyName: string): Integer; override;
    function KeyToProperty(const AKey: Integer): string; override;
  end;

function TJsonStringDictionaryConverter<V>.KeyToProperty(const AKey: Integer): string;
begin
  Result := AKey.ToString;
end;

function TJsonStringDictionaryConverter<V>.PropertyToKey(const APropertyName: string): Integer;
begin
  Result := APropertyName.ToInteger;
end;

とすれば、良いわけだ。

今回は、定義済みの、TJsonStringDictionaryConverterを使用して先ほどの

  TCoffieShopDicConverter = class(TJsonDictionaryConverter<string, TCoffeeShop>);


  TCoffieShopDicConverter = class(TJsonStringDictionaryConverter<TCoffeeShop>);

に修正し実行すれば、目的のJSON形式の文字列のファイルが作成できます。

ファイルの読み込みは前回と同様、次のコードになります。(確認用に読み込んだものメモに列挙しております。)

procedure TForm1.Button2Click(Sender: TObject);
var
  CoffeeShop : TCoffeeShop;
  CofeeShopList : TCofeeShopList;
  serializer: TJsonSerializer;
  s : string;
  Clerk : TClerk;
begin

  s := TFile.ReadAllText('CoffeeShop.Json',TEncoding.UTF8);

  serializer := TJsonSerializer.Create;
  try
    CofeeShopList := serializer.Deserialize<tcofeeshoplist>(s);
    try
      Memo1.Clear;
      Memo1.Lines.Add(CofeeShopList.TownName);
      for CoffeeShop in CofeeShopList.Shops.Values do
      begin
        Memo1.Lines.Add(CoffeeShop.ShopName);
        for Clerk in CoffeeShop.Clerks do
          begin
          Memo1.Lines.Add(Clerk.Name + '(' + Clerk.Age.ToString() + ')');
        end;
      end;

    finally
      CoffeeShop.Clerks.Clear();
      CoffeeShop.Free;
    end;
  finally
    serializer.Free
  end;

2018年5月3日木曜日

TokyoでJson形式のファイルの読み書きをして見た。

唐突ですが、Json.NET良いですよね。高度なこともできますが、Json形式の文字列のシリアル化/デシリアライズ化がだけで良いな簡単にできますしね。
Delphi(Tokyo)でJson形式のファイルを読み込む必要が出てきたので、Json.NETのような書き方ができなか調べていたところもう1年くらい前の記事になりますが、

@lynatan さんのTJsonSerializerの記事

TJsonSerializerの使い方。
TJsonSerializerの実用例

エンバカデロさんのブログ

TJsonSerializerでJSONに変換する[JAPAN]

DelphiでもTokyoになって、Json.NETのような書き方ができるようになっているとのことでしたので、試して見ました。

読み書きするのは、以下のような内容のファイル

{"ShopName":"ラビットハウス",
  "Clerks":[
    {"Name":"ココア","Age":17},
    {"Name":"チノ","Age":15},
    {"Name":"リゼ","Age":17}
  ]
}

上記の構造にマップできるクラスを作成します。
先ずは、従業員(Clerk)クラス。
パブリックメンバーをシリアライズ対象のするのでクラスに[JsonSerialize(TJsonMemberSerialization.&Public)]
属性を付加しています。
(余談ですが、予約語、指令と被るワードを使う場合、その前に"&"が必要です。)

unit Unit1;
  //パブリックメンバーをシリアライズ対象にする属性
  [JsonSerialize(TJsonMemberSerialization.&Public)]
  TClerk = class
    private
      FName: string;
      FAge: integer;

      procedure SetAge(const Value: integer);
      procedure SetName(const Value: string);
    public
      property Name : string read FName write SetName;
      property Age : integer read FAge write SetAge;
      constructor Create; overload;
      constructor Create(const vName : string; const vAge : integer); overload;
  end;

次に喫茶店(CoffeeShop)クラスの定義
  [JsonSerialize(TJsonMemberSerialization.&Public)]
  TCoffeeShop = class
    private
    FClerks: TList<TClerk>;
    FShopName: string;
    procedure SetClerks(const Value: TList<TClerk>);
    procedure SetShopName(const Value: string);
    public
      property ShopName : string read FShopName write SetShopName;
      
      //TClerkクラスのジェネリックリスト用のコンバーターを登録
      [JsonConverter(TJsonClerkListConverter)]
      property Clerks : TList<TClerk> read FClerks write SetClerks;

      public constructor Create;
  end;

メンバーは、喫茶店名と、従業員のリスト(ジェネリックのリスト)です。
こちらも、パブリックメンバーをシリアル化の対象とします。
ジェネリックのリストは、そのままではシリアライズできないので、Json.Converterユニットに定義済みの TJsonListConverterからTClerk型用の派生クラス

  //TClerk型のリスト用のコンバーター
  TJsonClerkListConverter = class(TJsonListConverter<TClerk>);

を作成し、TClerk型のジェネリックリスト型のメンバーClerks用のコンバーター属性を付加しています。

クラス定義の全体は、以下とおりです。
unit Unit2;

interface
uses
  //Json.SerializersとConverterを使用する。
    System.JSON.Serializers
  , System.JSON.Converters
  //TList<t>を使用する
  , System.Generics.Collections
  ;

type

  //パブリックメンバーをシリアライズ対象にする属性
  [JsonSerialize(TJsonMemberSerialization.&Public)]
  TClerk = class
    private
      FName: string;
      FAge: integer;

      procedure SetAge(const Value: integer);
      procedure SetName(const Value: string);
    public
      property Name : string read FName write SetName;
      property Age : integer read FAge write SetAge;
      constructor Create; overload;
      constructor Create(const vName : string; const vAge : integer); overload;
  end;

  //TClerk型のリスト用のコンバーター
  TJsonClerkListConverter = class(TJsonListConverter<TClerk>;);

  [JsonSerialize(TJsonMemberSerialization.&Public)]
  TCoffeeShop = class
    private
    FClerks: TList<TClerk>;
    FShopName: string;
    procedure SetClerks(const Value: TList<TClerk>);
    procedure SetShopName(const Value: string);
    public
      property ShopName : string read FShopName write SetShopName;
      
      //TClerkクラスのジェネリックリスト用のコンバーターを登録
      [JsonConverter(TJsonClerkListConverter)]
      property Clerks : TList<TClerk> read FClerks write SetClerks;

      public constructor Create;
  end;
implementation

{ TClerk }

constructor TClerk.Create(const vName: string; const vAge: integer);
begin
   FName := vName;
   FAge := vAge;
end;

constructor TClerk.Create;
begin

end;

procedure TClerk.SetAge(const Value: integer);
begin
  FAge := Value;
end;

procedure TClerk.SetName(const Value: string);
begin
  FName := Value;
end;

{ TCoffeeShop }

constructor TCoffeeShop.Create;
begin
  FClerks := TList<TClerk>.Create;
end;

procedure TCoffeeShop.SetClerks(const Value: TList<TClerk>);
begin
  FClerks := Value;
end;

procedure TCoffeeShop.SetShopName(const Value: string);
begin
  FShopName := Value;
end;

end.

上記で定義したクラスに対で、冒頭で示した内容のJson形式の定義ファイルCoffeeShop.Jsonを読み込み、デシリアライズする処理は、以下のとおりとなります。

procedure TForm1.Button2Click(Sender: TObject);
var
  CoffeeShop : TCoffeeShop;
  serializer: TJsonSerializer;
  s : string;
  Clerk : TClerk;
begin

  s := TFile.ReadAllText('CoffeeShop.Json',TEncoding.UTF8);

  serializer := TJsonSerializer.Create;
  try
    CoffeeShop := serializer.Deserialize<TCoffeeShop>(s);
    try
      Memo1.Clear;
      Memo1.Lines.Add(CoffeeShop.ShopName);
      for Clerk in CoffeeShop.Clerks do
      begin
        Memo1.Lines.Add(Clerk.Name + '(' + Clerk.Age.ToString() + ')');
      end;
    finally
      CoffeeShop.Clerks.Clear();
      CoffeeShop.Free;
    end;
  finally
    serializer.Free
  end;

ファイルを読み込み、シリアライザーを生成し、読み込んだJson文字列をデシリアライズして、ショップ名と、店員の情報をメモに表示しています。 (余談ですが、クラス定義で属性を使用しないで、TJsonSerializerインスタンスのConverterリストにTJsonClerkListConverterのインスタンスを登録してもデシリアライズできます。)

冒頭で示した内容のJson形式の定義ファイルを作成する場合は、以下の処理でできます。

procedure TForm1.Button1Click(Sender: TObject);
var
  CoffeeShop : TCoffeeShop;
  serializer: TJsonSerializer;
  s : string;


begin
  CoffeeShop := TCoffeeShop.Create;
  try
    CoffeeShop.ShopName := 'ラビットハウス';
    CoffeeShop.Clerks.Add(TClerk.Create('ココア',17));
    CoffeeShop.Clerks.Add(TClerk.Create('チノ',15));
    CoffeeShop.Clerks.Add(TClerk.Create('リゼ',17));

    serializer := TJsonSerializer.Create;
    try

      s := serializer.Serialize(CoffeeShop);
      Memo1.Text := s;

      TFile.WriteAllText('CoffeeShop.Json',s);

    finally
      serializer.Free;
    end;

  finally
    CoffeeShop.Clerks.Clear();
    CoffeeShop.Free;
  end;
end;

TCoffeeShop型のインスタンスを生成し、メンバーを設定後、シリアライザーを生成しシリアライズ後、ファイルに保存しています。
(確認の為、画面上のメモにも表示しています。)


プロジェクト一式は、https://bitbucket.org/OldTPFun/delphitest/src/master/JsonTest/Proj1/
に配置しております。