6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

子要素が動的に変わるJSONをC#でマッピングするときはちょっと大変(System.Text.Json)

Posted at

(こちらも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によりシリアライズするクラスを変える

マッピングする先のクラス定義

class.cs
// 基底クラス、共通する要素があるならここに入れとく、プロパティが空でも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)

コンバーター実装

https://docs.microsoft.com/ja-jp/dotnet/standard/serialization/system-text-json-converters-how-to?pivots=dotnet-6-0#converter-samples-for-common-scenarios
まずは公式からのサンプル

converter.cs
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);
}

もひとつ

converter.cs
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);

大変だった

どう調べていいかわからずまず右往左往、公式にあったが今回のケースにはまるわけじゃないので、色々と検証必要だった。

6
6
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
6
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?