JsonConverter<T> を使って多態化されたオブジェクトを JsonSerializer でシリアライズ・デシリアライズする
前の記事に記載した通り、.NET Core 3.1 から追加された JsonSerializer はポリモーフィズムには対応していません。
そのため、JsonConverter<T>
を使って自前で対応する必要があります。
この記事では前提として以下の名前空間の宣言とクラスが存在している事とします。
using System.Text.Json; using System.Text.Json.Serialization; public abstract class GameMachine { public GameMachine(int price) { Price = price; } public int Price { get; init; } } public class NintendoSwitch : GameMachine { public NintendoSwitch(bool isLite, int price) : base(price) { IsLite = isLite; } public bool IsLite { get; init; } } public class XboxSeries : GameMachine { public XboxSeries(EditionType edition, int price) : base(price) { Edition = edition; } public EditionType Edition { get; init; } public enum EditionType { X, S } } public class PlayStation5 : GameMachine { public PlayStation5(bool isDigitalOnly, int price) : base(price) { IsDigitalOnly = isDigitalOnly; } public bool IsDigitalOnly { get; init; } }
JsonConvert<T>
無しでの挙動を確認する
基底クラスに触れない形で実装。
public class GameCatalog { public NintendoSwitch NintendoSwitch { get; } public XboxSeries Xbox { get; } public PlayStation5 PlayStation { get; } public GameCatalog(NintendoSwitch nintendoSwitch, XboxSeries xbox, PlayStation5 playStation) { NintendoSwitch = nintendoSwitch; Xbox = xbox; PlayStation = playStation; } } void Main() { var catalog = new GameCatalog( new NintendoSwitch(true, 20000), new XboxSeries(XboxSeries.EditionType.X, 50000), new PlayStation5(false, 60000) ); var json = JsonSerializer.Serialize(catalog); var catalog2 = JsonSerializer.Deserialize<GameCatalog>(json); }
すると、正常にシリアライズ・デシリアライズが出来ている事が分かります。
続いてこれを基底クラスを使って実装。
public class GameCatalog { public GameMachine[] GameMachines { get; } public GameCatalog(params GameMachine[] gameMachines) { GameMachines = gameMachines; } } void Main() { var catalog = new GameCatalog( new NintendoSwitch(true, 20000), new XboxSeries(XboxSeries.EditionType.X, 50000), new PlayStation5(false, 60000) ); var json = JsonSerializer.Serialize(catalog); var catalog2 = JsonSerializer.Deserialize<GameCatalog>(json); }
これを実行すると Deserialize 時に以下のエラーが発生します。
Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'GameMachine'
ポリモーフィズムに対応していないのでこうなるのは正しい挙動です。 一方で、Serialize 時にはエラーが出ておらず、正常に実行されたように見えますが、実際は以下のJSON が出力されており、基底クラスで定義されている物しか出力されていないことが分かります。
{"GameMachines":[{"Price":20000},{"Price":50000},{"Price":60000}]}
それでは JsonConverer<T>
を実装してポリモーフィズムに対応してみます。
JsonConverter<T>
の実装
JsonConverter<GameMachine>
を実装して GameMachine のポリモーフィズムに対応します。
JsonConverter の実装はこちら。コメントで色々と
public class GameMachineConverter : JsonConverter<GameMachine> { /// <summary> /// 識別子 /// </summary> private enum Discriminator { NintendoSwitch, XboxSeries, PlayStation5 } /// <summary> /// GameMachine を継承していたら変換可能とする /// </summary> public override bool CanConvert(Type typeToConvert) => typeof(GameMachine).IsAssignableTo(typeToConvert); /// <summary> /// 抽象メソッドの JsonConverter<T>.Read を実装。 /// Deserialize 時に呼ばれる。 /// 識別子を元にインスタンス化するクラスを選択する。 /// </summary> /// <remarks> /// コメント上では前提として value が NintendoSwitch であることとする /// </remarks> public override GameMachine Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { // {"Discriminator":0,"Value":{"Price":21978,"IsLite":true}} を順に読んでいく // 初期状態では "{" が読み込まれているはず if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException("オブジェクト開始文字から始まっていない"); // 1つ読み進める // プロパティ名が読み込まれる: Discriminator if (!reader.Read() || reader.TokenType != JsonTokenType.PropertyName || reader.GetString() != "Discriminator") throw new JsonException("Discriminator が見つからない"); // 1つ読み進める // Discriminator プロパティにセットされている値が読み込まれる: 0 if (!reader.Read() || reader.TokenType != JsonTokenType.Number) throw new JsonException("Discriminator の値が数値ではない"); // Discriminator プロパティの値を取得する: 0 var discriminator = (Discriminator)reader.GetInt32(); // 1つ読み進める // プロパティ名が読み込まれる(Value) if (!reader.Read() || reader.GetString() != "Value") throw new JsonException("Value が見つからない"); // 1つ読み進める // "Value": の次は { なのでオブジェクト開始文字 "{" が読み込まれる if (!reader.Read() || reader.TokenType != JsonTokenType.StartObject) throw new JsonException("オブジェクトが見つからない"); // Discriminator を元に型を指定してデシリアライズする // ここで early return 出来そうだけどしちゃだめで、reader が最後の "}" まで読み込んでから return する必要がある var result = (GameMachine)JsonSerializer.Deserialize(ref reader, GetTypeByDiscriminator(discriminator)); // 1つ読み進めてオブジェクト終了文字まで読み込む if (!reader.Read() || reader.TokenType != JsonTokenType.EndObject) throw new JsonException("オブジェクト終了文字が見つからない"); return result; } /// <summary> /// Discriminator から型情報を取得する /// </summary> private Type GetTypeByDiscriminator(Discriminator discriminator) { switch (discriminator) { case Discriminator.NintendoSwitch: return typeof(NintendoSwitch); case Discriminator.XboxSeries: return typeof(XboxSeries); case Discriminator.PlayStation5: return typeof(PlayStation5); default: throw new IndexOutOfRangeException("未定義の Discriminator が指定された"); } } /// <summary> /// 抽象メソッドの JsonConverter<T>.Write を実装。 /// Serialize 時に呼ばれる。 /// 派生クラスを識別可能とするために識別子的なものを付与した JSON を出力させる。 /// JsonSerializerOptions は今回は使わない /// </summary> /// <remarks> /// コメント上では前提として value が NintendoSwitch であることとする /// </remarks> public override void Write(Utf8JsonWriter writer, GameMachine value, JsonSerializerOptions options) { // "{" を書き込む // { writer.WriteStartObject(); // 識別子を書き込む // {"Discriminator":0 writer.WriteNumber("Discriminator", (int)GetDiscriminator(value)); // value を格納するプロパティを書き込む // {"Discriminator":0,"Value": writer.WritePropertyName("Value"); // value をシリアライズする // {"Discriminator":0,"Value":{"Price":21978,"IsLite":true} JsonSerializer.Serialize(writer, value, value.GetType()); // "}" を書込 // {"Discriminator":0,"Value":{"Price":21978,"IsLite":true}} writer.WriteEndObject(); } /// <summary> /// 識別子を取得する /// </summary> private Discriminator GetDiscriminator(GameMachine value) { switch (value) { case NintendoSwitch: return Discriminator.NintendoSwitch; case XboxSeries: return Discriminator.XboxSeries; case PlayStation5: return Discriminator.PlayStation5; default: throw new IndexOutOfRangeException("未定義の型が指定された"); } } }
これを実装した上で先ほど失敗したコードを実行してみます。
var catalog = new GameCatalog( new NintendoSwitch(true, 20000), new XboxSeries(XboxSeries.EditionType.X, 50000), new PlayStation5(false, 60000) ); var json = JsonSerializer.Serialize(catalog); var catalog2 = JsonSerializer.Deserialize<GameCatalog>(json);
正常に動作するようになりました。
これくらいのコードなら自動生成もできると思うので必要になったら自動生成を実装すると良さそうです。
System.Text.Json.JsonSerializer のジェネリック版関数について
.NET Core 3.1 から JsonSerializer
が入ったので今更ながら試してみようかと思った触ってみた時に遭遇した、ちょっと考えればまぁ分からないでもないけど罠だなぁ…っていう挙動を覚書。
JsonSerializer について
JsonSerializer は .NET Core 3.1 から導入されたシリアライザです。
JsonSerializer
にはジェネリック版と非ジェネリック版の Serialize, Deserialize が用意されています。
基本的にはジェネリック版で良いと思いますが、ポリモーフィズムを持ったオブジェクトを扱う際には注意が必要になります。
ちなみにポリモーフィズムを持ったオブジェクトのデシリアライズにはデフォルトでは対応していないので、JsonConverter<T>
を継承して独自実装する必要があります。
これは後日。
実際に困る場合の例はこちら。
public class Program { public abstract class Item { public int Price { get; init; } public string Name { get; init; } } public class Fruit : Item { public int SugarContent { get; init; } } public class Meat : Food { public int FatLevel { get; init; } } public Main() { Item item = new Fruit { Price = 100, Name = "りんご", SugarContent = 14 }; var json = JsonSerializer.Serialize(item); // 以下略 } }
こうした時に、json の中身には Item が持っているプロパティしか存在せず、情報が欠落してしまいます。
Item 型の変数に入れた状態で Serialize したため <T>
が Item になった結果です。
これを回避するためには
- 具体的な型の変数に入れる
- 非ジェネリック版の関数を使って
Serialize(item, item.GetType())
とする JsonConverter<Item>
を実装して派生クラスの扱いを記述する
の3つが選択肢に上がります。
現実的にはには Item の配列とかが持ちたいケースが大半だったりすると思うので、JsonConverter 一択になるんじゃないかとは思います。
JsonConverter の実装が面倒な場合はシリアライズ、デシリアライズ時に具体的な型を指定するような構造にすることが必要になります。その場合は非ジェネリック版の関数を必ず使う、とかしないと事故るケースが出てきそうです。*1
*1:とはいえ、そんなことは書き捨てのコード以外ではやらないと思いますが
JavaScript における yield の話とかアロー関数の話とか
JavaScript で業務用のブックマークレットでも書くかと思って書いてたら何か自分が知ってる JavaScript からだいぶ変わってたのでメモがてら。
最近の JavaScript について
もはや JavaScript を勉強したのは何年前だって状態なので、間の機能を補完しようかと思ったのですが、何が増えているのかさっぱりわからないのでとりあえず yield
と ラムダ式
アロー関数
の話でも。async/await
もいいんですが、まぁこれは気が向いたら。
yield について
yield
は、委譲するとか譲るとかの意味を持った英単語で、実際 yield
の使用時には値を実処理側に委譲するような動きをします。
記述方法は以下の形式です。
function* sample() { yield "one"; yield "two"; yield "three"; }
function
の後にアスタリスクを付けることで yield
キーワードが使えるようになります。
この関数の戻り値は Generator
型となり、Generator.prototype.next()
関数や for of
*1 を使って要素を一つずつ処理することができます。
let values = sample(); console.log(values.next()); // {value: "one", done: false} console.log(values.next()); // {value: "two", done: false} console.log(values.next()); // {value: "three", done: false} console.log(values.next()); // {value: undefined, done: true}
ループで回して値を取得する場合は
let values = sample(); while ((let next = values.next()).done === false) { console.log(next.value); }
または
let values = sample(); for (const value of values) { console.log(value); }
で利用できます。基本は後者ですかね。
これができると何が嬉しいかというと、列挙処理において遅延評価がしやすくなる点と、同じことをしようとした場合に Iterator パターンを一から実装しないといけなかったものが簡潔に書けるようになった点にあります。
C# でも yield return
でほぼ同様のことができる実現でき、それについてはこちらに諸々まとめてあります。
Generator 型について
さて、C# を慣れ親しんだ身としては yield
で返された値は LINQ
っぽく扱いたくなるもので、これは Generator
の prototype
を拡張してやればできるはずです。
とりあえず拡張して U Select<T, U>(this IEnumerable<T, U> source, Func<T, U> selector)
的なものを足してみましょう。
Generator.prototype.select = function*(seletor){for (item of this) { yield selector(item); }};
すると、Generator
が見つからず、エラーになってしまいます。
Generator
は直接インスタンスを生成することが許可されていないため、触れる場所にはありません。
では yield
で返ってきた Generator
の prototype
を使ってみましょう。
const generator = (function*(){})(); generator.__proto__.select = function*(seletor){for (item of this) { yield selector(item); }};
できたっぽいので使ってみましょう。
const sample = (function*() { yield 1; yield 2; yield 3; })(); for (const item of sample.select(i => i * 10)) { console.log(item); }
実行すると以下のエラーが発生します。
VM2202:7 Uncaught TypeError: s.select is not a function or its return value is not iterable
何故でしょうか?
C# で yield return
に触れていると何となく原因はこれなのかな?
というものが頭に浮かびます。
yield
は結局のところシンタックスシュガーで、Generator は実行エンジン側で動的に生成された型になっているのです。
つまり Generator
を生成する関数毎に異なる型が返却されていることになります。
そこでデバッグコンソールからオブジェクトのプロトタイプチェーンを眺めてみると
どうやら Generator の上にさらに Generator がいました。
こいつがどうやら大元っぽいのでこいつを拡張してみましょう。
const generator = (function*(){})(); generator.__proto__.__proto__.select = function*(seletor){for (item of this) { yield selector(item); }};
で、これを使ってみましょう。
const sample = (function*() { yield 1; yield 2; yield 3; })(); for (const item of sample.select(i => i * 10)) { console.log(item); }
動きました。
これで Generator
の拡張ができるようになりました。
any や all も欲しいのでこちらも実装してみましょう。
今回は Generator
を返す必要がないので折角なのでアロー関数で書いてみましょう。
generator.__proto__.__proto__.any = (predicator = (_ => true)) => { for (const item of this) { if (predicator(item)) { return true; } return false; } }
さて実行してみましょう。
const sample = (function*() { yield 1; yield 2; yield 3; })(); if (sample.any()) { console.log("success"): }
実行すると以下のエラーが発生します。
VM1280:2 Uncaught TypeError: this is not iterable at Generator.generator.__proto__.__proto__.any
this が iterable じゃないと言われています。なるほど?
試しに function で再実装してみましょう。
generator.__proto__.__proto__.any = function(predicator = (_ => true)) { for (const item of this) { if (predicator(item)) { return true; } return false; } }
そして実行。
if (sample.any()) { console.log("success"): }
見事動きました。何故でしょう?
MDN を見ると以下のように記述されています。
this, arguments, super, new.target を束縛しません。
つまり this は記述されている環境に依存した値のまま変わらないということになります。
アロー関数は関数の引数に使用するなどに止めるのが良さそうですね。
*1:iterable protocol を満たす場合に for of を使うことができる
楽天モバイル(MNO)で eSIM のアクティベートに失敗した話
前置き
https://network.mobile.rakuten.co.jp/cms/faq/detail/00000890
楽天回線でeSIMに対応している製品はRakuten Miniのみとなります。 その他のeSIM対応製品の動作確認は保証しておりませんのでご了承ください。
と、あるように Rakuten Mini 以外での動作は保証されていませんのでご注意ください。
続きを読むiPhone XS Max レビュー
iPhone XS Max を早速購入して色々と思うところがあったので簡単にレビューしてみようと思う。
続きを読む