(こちらもNewtonsoftの方でなく、System.Text.Json
要素が動的に変わるJSONはC#で扱いにくい
例
{
"id": "1",
"type": "A",
"attributes": {
(ここの定義が上のtypeにより変わる)
}
}
dynamic
型使ってもできるが、できればクラスを定義して使いたいとき
ポリモーフィックなJSONを扱うには、という感じで公式でも解説されてはいる。
https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-6-0#support-polymorphic-deserialization
コーディング指針
- 各attributesに共通する要素を持つ親クラスを作る(共通してなかったら空でも良い)
- Converter内でtypeによりシリアライズするクラスを変える
マッピングする先のクラス定義
// 基底クラス、共通する要素があるならここに入れとく、プロパティが空でもOK
public class BaseAttributes
{
}
public class A : BaseAttributes
{
public int id { get; set; }
}
public class B : BaseAttributes
{
public int price { get; set; }
}
// 他にもあれば
なお、基底クラスをabstract
宣言しているとエラーになるので、注意(Newtonsoftの場合のコードと混同してやってた)
Unhandled exception. System.NotSupportedException: Deserialization of reference types without parameterless constructor is not supported. Type 'JsonConverterTutorial.BaseAttributes'
at System.Text.Json.ThrowHelper.ThrowNotSupportedException_DeserializeCreateObjectDelegateIsNull(Type invalidType)
コンバーター実装
public class ObjectToInferredTypesConverter : JsonConverter<object>
{
public override object Read(
ref Utf8JsonReader reader,
Type typeToConvert,
JsonSerializerOptions options) => reader.TokenType switch
{
JsonTokenType.True => true,
JsonTokenType.False => false,
JsonTokenType.Number when reader.TryGetInt64(out long l) => l,
JsonTokenType.Number => reader.GetDouble(),
JsonTokenType.String when reader.TryGetDateTime(out DateTime datetime) => datetime,
JsonTokenType.String => reader.GetString()!,
_ => JsonDocument.ParseValue(ref reader).RootElement.Clone()
};
public override void Write(
Utf8JsonWriter writer,
object objectToWrite,
JsonSerializerOptions options) =>
JsonSerializer.Serialize(writer, objectToWrite, objectToWrite.GetType(), options);
}
もひとつ
using System.Text.Json;
using System.Text.Json.Serialization;
namespace SystemTextJsonSamples
{
public class PersonConverterWithTypeDiscriminator : JsonConverter<Person>
{
public override Person Read(
ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
if (reader.TokenType != JsonTokenType.StartObject)
{
throw new JsonException();
}
reader.Read();
if (reader.TokenType != JsonTokenType.PropertyName)
{
throw new JsonException();
}
string? propertyName = reader.GetString();
if (propertyName != "TypeDiscriminator")
{
throw new JsonException();
}
reader.Read();
if (reader.TokenType != JsonTokenType.Number)
{
throw new JsonException();
}
TypeDiscriminator typeDiscriminator = (TypeDiscriminator)reader.GetInt32();
Person person = typeDiscriminator switch
{
TypeDiscriminator.Customer => new Customer(),
TypeDiscriminator.Employee => new Employee(),
_ => throw new JsonException()
};
while (reader.Read())
{
if (reader.TokenType == JsonTokenType.EndObject)
{
return person;
}
if (reader.TokenType == JsonTokenType.PropertyName)
{
propertyName = reader.GetString();
reader.Read();
switch (propertyName)
{
case "CreditLimit":
decimal creditLimit = reader.GetDecimal();
((Customer)person).CreditLimit = creditLimit;
break;
case "OfficeNumber":
string? officeNumber = reader.GetString();
((Employee)person).OfficeNumber = officeNumber;
break;
case "Name":
string? name = reader.GetString();
person.Name = name;
break;
}
}
}
throw new JsonException();
}
// read以外は省略
}
後者のサンプルを見ると、どうやらreader
でJSONの要素を順次読み込みしていっているみたいだ。
となると、今回の例だとtype
の値によりattributes
を割り当てるクラスを変えれば良さそうだ。
ただ、ここだとattributes
の部分もオブジェクトになっているので、もとの文字列がほしい。
readerから元のJSON string取得
using (var jsonDoc = JsonDocument.ParseValue(ref reader))
{
valueString = jsonDoc.RootElement.GetRawText();
}
reader
からもとの文字列を取得してくる。
tokenType
でプロパティなのかなんなのか(いろいろあるみたい)判定できる。
実際に書いたコードを一部抜粋して
if (reader.TokenType == JsonTokenType.PropertyName)
{
string propertyName = reader.GetString()!;
reader.Read();
var valueString = "";
using (var jsonDoc = JsonDocument.ParseValue(ref reader))
{
valueString = jsonDoc.RootElement.GetRawText();
}
switch (propertyName)
{
case "id":
iv.id = reader.GetString();
break;
case "type":
iv.type = reader.GetString();
attrType = iv.type;
break;
case "attributes":
switch (attrType)
{
case "A":
iv.attributes = JsonSerializer.Deserialize<A>(valueString);
break;
case "B":
iv.attributes = JsonSerializer.Deserialize<B>(valueString);
break;
case "C":
iv.attributes = JsonSerializer.Deserialize<C>(valueString);
break;
}
break;
}
}
こんな感じでtype
の値によりデシリアライズする先のクラスを動的に変える。
動かすときのコードは
var obj = JsonSerializer.Deserialize<BaseAttributes>(json, new JsonSerializerOptions() {Converters = { new AttributesConverter() }});
で、OK
ただ、実際にattributes
にアクセスするときはちゃんとキャストしよう。
Console.WriteLine(((A)obj3.attributes).aprop);
大変だった
どう調べていいかわからずまず右往左往、公式にあったが今回のケースにはまるわけじゃないので、色々と検証必要だった。