본문 바로가기
C#

C#] Polymorphic Serialization and Deserialization with System.Text.Json

by Fastlane 2022. 8. 10.
728x90
반응형

출처 : https://code-maze.com/csharp-polymorphic-serialization-and-deserialization/

System.Text.Json이란?

.NET개발자가 JSON format을 handling할 수 있는 powerful한 package이다. 

 

System.Text.Json 소개 

 

C#] System.Text.Json vs Newtonsoft.Json 차이점 비교

MS는 2019년 .Net Core 3.0과 함께 System.Text.Json namespace를 release했다. Newtonsoft.Json 에서 System.Text.Json으로 migration을 고려하게 되었다. Newtonsoft.Json과 비교하며, migration 장단점을 확인..

bigexecution.tistory.com

예시 Class

Member란 base class와 이를 상속받는 Student, Professor class를 예로 살펴보자. 

public abstract class Member
{
    public string? Name { get; set; }
 
    public DateTime BirthDate { get; set; }
}

public class Student : Member
{
    public int RegistrationYear { get; set; }
 
    public List<string> Courses { get; set; } = new List<string>();
}

public class Professor : Member
{
    public string? Rank { get; set; }
 
    public bool IsTenured { get; set; }
}

Student, Professor objects를 포함하는 Member base type array를 생성해보자. 

var members = new Member[]
{
    new Student()
    {
        Name = "John Doe",
        BirthDate = new DateTime(2000, 2, 4),
        RegistrationYear = 2019,
        Courses = new List<string>()
        {
            "Algorithms",
            "Databases",
        }
    },
    new Professor()
    {
        Name = "Jane Doe",
        BirthDate = new DateTime(1978, 6, 6),
        Rank = "Full Professor",
        IsTenured = true,
    },
    new Student()
    {
        Name = "Jason Doe",
        BirthDate = new DateTime(2002, 7, 8),
        RegistrationYear = 2020,
        Courses = new List<string>()
        {
            "Databases"
        }
    }
};

Polymorphic Serialization With System.Text.Json

JsonSerializer의 Serialize함수를 사용하여 array를 serialize해보자. 

var options = new JsonSerializerOptions
{
    WriteIndented = true
};

var membersJson = JsonSerializer.Serialize<Member[]>(members, options);

결과적으로, 우리는 파생 클래스에서 정의한 property가 포함되지 않은 JSON string을 얻는다. 

[
  {
    "Name": "John Doe",
    "BirthDate": "2000-02-04T00:00:00"
  },
  {
    "Name": "Jane Doe",
    "BirthDate": "1978-06-06T00:00:00"
  },
  {
    "Name": "Jason Doe",
    "BirthDate": "2002-07-08T00:00:00"
  }
]

함수는 base class의 properties만 serialize하였다. 이것은 우연히 파생 클래스의 민감정보가 serialize되는 것을 방지한다. 

파생클래스의 properties까지 serialize하려면 아래와 같이 member array에서 object array로 수정한다. 

var membersJson = JsonSerializer.Serialize<object[]>(members, options);

부모 클래스, 자식 클래스 property가 모두 담긴 JSON string을 확인할 수 있다. 다만, serialized object properties의 순서는 자식클래스 properties에서 부모클래스 properties 순으로 표시된다. Name, BirthDate property가 뒤로 나온다. 

[
  {
    "RegistrationYear": 2019,
    "Courses": [
      "Algorithms",
      "Databases"
    ],
    "Name": "John Doe",
    "BirthDate": "2000-02-04T00:00:00"
  },
  {
    "Rank": "Full Professor",
    "IsTenured": true,
    "Name": "Jane Doe",
    "BirthDate": "1978-06-06T00:00:00"
  },
  {
    "RegistrationYear": 2020,
    "Courses": [
      "Databases"
    ],
    "Name": "Jason Doe",
    "BirthDate": "2002-07-08T00:00:00"
  }
]

Polymorphic Deserialization With System.Text.Json

serialization case와는 반대로, JSON string을 deserialization하기 위한 단순한 방법은 없다. deserializer는 string으로부터 적합한 type의 object를 찾을 수 없다. 

 

Custom converter가 적합한 type의 object를 찾을 수 있도록 어떻게 할 수 있는가?

한 가지 방법은, object의 특정 properties를 찾는 것이다. 예를들면, Courses property는 Student class에만 존재한다. 

또 다른 방법은, class name을 사용하는 것이다. 적합한 class를 명시하기 위해, JSON object안에 discriminator property를 포함할 수 있다. discriminator property는 class정의에 없지만, serialization 동안 만들어지고, deserialization 동안 읽힌다. 

 

JSON string을 적합한 object로 parse하기 위해, custom conver로 마지막 접근방법을 적용해보자. 

 

JsonConverter<T>를 override하는  UniversityJsonConverter를 생성해보자. 

public class UniversityJsonConverter : JsonConverter<Member>
{
    public override bool CanConvert(Type typeToConvert) =>
        typeof(Member).IsAssignableFrom(typeToConvert);

    public override Member Read(ref Utf8JsonReader reader, 
        Type typeToConvert, JsonSerializerOptions options)
    { }

    public override void Write(Utf8JsonWriter writer, 
        Member member, JsonSerializerOptions options)
    { }
}

Overriding the JsonConverter Read Method

deserialization을 수행하기 위해, Read method를 override하자. 

public override Member Read(ref Utf8JsonReader reader, 
    Type typeToConvert, JsonSerializerOptions options)
{
    // StartObject token은 JSON object의 시작을 표시한다. 
    if (reader.TokenType != JsonTokenType.StartObject)
        throw new JsonException();

    reader.Read();
    if (reader.TokenType != JsonTokenType.PropertyName)
        throw new JsonException();

    string? propertyName = reader.GetString();
    if (propertyName != "MemberType")
        throw new JsonException();

    reader.Read();
    if (reader.TokenType != JsonTokenType.String)
        throw new JsonException();

    var memberType = reader.GetString();
    Member member;
    //type discriminator를 읽고, 적합한 Member object를 생성한다. 
    switch (memberType)
    {
        case "Student":
            member = new Student();
            break;

        case "Professor":
            member = new Professor();
            break;

        default:
            throw new JsonException();
    };
    while (reader.Read())
    {
        // object 종료 시, member를 반환한다. 
        if (reader.TokenType == JsonTokenType.EndObject)
            return member;
        if (reader.TokenType == JsonTokenType.PropertyName)
        {
            propertyName = reader.GetString();
            reader.Read();
            //PropertyName token을 사용하여 property name을 찾는다. 
            switch (propertyName)
            {
                case "Name":
                    member.Name = reader.GetString();
                    break;
                case "BirthDate":
                    member.BirthDate = reader.GetDateTime();
                    break;
                case "RegistrationYear":
                    int registrationYear = reader.GetInt32();
                    if (member is Student)
                        ((Student)member).RegistrationYear = registrationYear;
                    else
                        throw new JsonException();
                    break;
                case "Rank":
                    string? rank = reader.GetString();
                    if (member is Professor)
                        ((Professor)member).Rank = rank;
                    else
                        throw new JsonException();
                    break;
                case "IsTenured":
                    bool isTenured = reader.GetBoolean();
                    if (member is Professor)
                        ((Professor)member).IsTenured = isTenured;
                    else
                        throw new JsonException();
                    break;
                case "Courses":
                    if (member is Student)
                    {
                        if (reader.TokenType == JsonTokenType.StartArray)
                        {
                            while (reader.Read())
                            {
                                if (reader.TokenType == JsonTokenType.EndArray)
                                    break;
                                var course = reader.GetString();
                                if (course != null)
                                    ((Student)member).Courses.Add(course);
                            }
                        }
                    }
                    else
                        throw new JsonException();
                    break;
            }
        }
    }
    throw new JsonException();
}

Overriding the JsonConverter Write Method

discriminator property인 MemberType을 먼저 작성하고, MemberType에 따라 properties를 작성한다. 

Write 함수는 원하는 순서대로 JSON string을 생성할 수 있게 한다. 

public override void Write(Utf8JsonWriter writer, Member member, JsonSerializerOptions options)
{
    writer.WriteStartObject();

    if (member is Student student)
    {
        writer.WriteString("MemberType", "Student");
        writer.WriteString("Name", member.Name);
        writer.WriteString("BirthDate", member.BirthDate);
        writer.WriteNumber("RegistrationYear", student.RegistrationYear);
        writer.WriteStartArray("Courses");
        foreach(var course in student.Courses)
        {
            writer.WriteStringValue(course);
        }
        writer.WriteEndArray();

    }
    else if (member is Professor professor)
    {
        writer.WriteString("MemberType", "Professor");
        writer.WriteString("Name", member.Name);
        writer.WriteString("BirthDate", member.BirthDate);
        writer.WriteString("Rank", professor.Rank);
        writer.WriteBoolean("IsTenured", professor.IsTenured);
    }

    writer.WriteEndObject();
}

이제 JsonSerializerOptions object 선언에 생성한 custom converter를 사용해보자. 

시작부분의 members 초기 array와 동일한 newMembers array를 결과로 확인할 수 있다. 

var options = new JsonSerializerOptions
{
    Converters = { new UniversityJsonConverter() },
    WriteIndented = true
};

var members = new Member[] { ... }  // the member array introduced at the beginning of the article

var membersJson = JsonSerializer.Serialize<Member[]>(members, options);
var newMembers = JsonSerializer.Deserialize<Member[]>(membersJson, options);

 

 

 

728x90
반응형

댓글