본문 바로가기
Programming/.Net(C#)

다형성 유형 직렬화(Serialize Polymorphic Types)

by _S0_H2_ 2024. 6. 27.
728x90
반응형

공식문서를 옮기며 학습한 내용입니다. (.NET 8)

 

 

1. 파생 클래스의 속성 직렬화

.NET 7부터 System.Text.Json특성 주석을 사용한 다형성 유형 계층 직렬화 및 역직렬화를 지원함.

 

Base class와 파생 class 하나씩 정의한다. (Base class에 정의한 attribute는 링크 참조)

[JsonDerivedType(typeof(WeatherForecastWithCity))]
public class WeatherForecastBase
{
    public DateTimeOffset Date { get; set; }
    public int TemperatureCelsius { get; set; }
    public string? Summary { get; set; }
}

 

public class WeatherForecastWithCity : WeatherForecastBase
{
    public string? City { get; set; }
}

 

 

이 시나리오에서 WeatherForecastWithCityWeatherForecastBase로 역직렬화하면 런타임 타입이 WeatherForecastWithCity 일 때 그 속성들이 포함되지 않는다.

WeatherForecastBase value = JsonSerializer.Deserialize<WeatherForecastBase>("""
    {
      "City": "Milwaukee",
      "Date": "2022-09-26T00:00:00-05:00",
      "TemperatureCelsius": 15,
      "Summary": "Cool"
    }
    """);

Console.WriteLine(value is WeatherForecastWithCity); 
// False


var json = JsonSerializer.Serialize(value, options);
Console.WriteLine($"WeatherForecastWithCity: {json}");
// WeatherForecastWithCity: {
//   "Date": "2022-09-26T00:00:00-05:00",
//   "TemperatureCelsius": 15,
//   "Summary": "Cool"
// }

 

 

 

2. 다형성 유형 판별자

다형성 역직렬화를 활성화하려면 파생 클래스에 대해 type discriminator 를 지정해야한다.

    [JsonDerivedType(typeof(WeatherForecastBase), typeDiscriminator:"base")]
    [JsonDerivedType(typeof(WeatherForecastWithCity), typeDiscriminator:"withCity")]
    public class WeatherForecastBase
    {
        public DateTimeOffset Date { get; set; }
        public int TemperatureCelsius { get; set; }
        public string? Summary { get; set; }
    }

 

이 때, WeatherForecastBase를 직렬화하면 $type으로 지정한 typeDiscriminator가 정의되고,

WeatherForecastBase weather = new WeatherForecastWithCity
{
    City = "Milwaukee",
    Date = new DateTimeOffset(2022, 9, 26, 0, 0, 0, TimeSpan.FromHours(-5)),
    TemperatureCelsius = 15,
    Summary = "Cool"
};
var json2 = JsonSerializer.Serialize<WeatherForecastBase>(weather, options);
Console.WriteLine(json2);
//{
//   "$type": "withCity", <--------------------------- 여기
//  "City": "Milwaukee",
//  "Date": "2022-09-26T00:00:00-05:00",
//  "TemperatureCelsius": 15,
//  "Summary": "Cool"
//}

 

역직렬화 하면 파생 클래스인 WeatherForecastWithCity 임을 알 수 있다.

WeatherForecastBase? value2 = JsonSerializer.Deserialize<WeatherForecastBase>(json2);
Console.WriteLine(value2 is WeatherForecastWithCity);
// True

 

 ★이 때, typeDiscriminator 는 메타데이터 속성 ($id, $ref) 과 함께 사용되기 때문에 typeDiscriminator를 JSON 객체의 시작 부분에 두어야 한다.   

 

다음을 역직렬화 하려고 할 때 $type이 처음에 위치하지 않으면 다음과 같은 에러가 발생한다.

var json = """
{
   "City": "Milwaukee",
   "$type": "withCity", <----------------------------- 첫번째가 아니면?
   "Date": "2022-09-26T00:00:00-05:00",
   "TemperatureCelsius": 15,
   "Summary": "Cool"
}
""";
WeatherForecastBase? value = JsonSerializer.Deserialize<WeatherForecastBase>(json);

 

'The metadata property is either not supported by the type or is not the first property in the deserialized JSON object. Path: $.$type | LineNumber: 2 | BytePositionInLine: 11.'

 

AllowOutOfOrderMetadataProperties 옵션이 추가되었으며 해당 내용은 .NET 9 Preview 2 부터 반영된다. (test해보니 아주 만족 *-*) 

 

 

3. Mix and Match Type Discriminator formats

typeDiscriminator로 int와 string을 사용할 수 있고, int와 string을 혼합하여 사용할 수는 있지만 권장되는 방법은 아니다.

    [JsonDerivedType(typeof(WeatherForecastBase), 0)]
    [JsonDerivedType(typeof(WeatherForecastWithCity), "withCity")]

 

새롭게 모델을 정의하고 두 개의 파생 클래스를 생성하였다.

[JsonDerivedType(typeof(BigCity), "big")]
[JsonDerivedType(typeof(PopularCity), "popular")]
public class BaseCity
{
    public string Name { get; set; } = "Seoul";
    public string? Description { get; set; } = "capital city";
}

public class BigCity : BaseCity
{
    public int Size { get; set; } = 1000;
}

public class PopularCity : BaseCity
{
    public int SightSeeing { get; set; } = 2000;
}

 

각 클래스의 객체를 JSON으로 직렬화하고 역직렬화하여 확인한 내용을 출력한다.

public static void ConfirmCity<T>() where T : BaseCity, new()
{
    var json = JsonSerializer.Serialize<BaseCity>(new T());
    Console.WriteLine(json);

    BaseCity? result = JsonSerializer.Deserialize<BaseCity>(json);
    Console.WriteLine($"result is {typeof(T)}; // {result is T}");
    Console.WriteLine();
}

 

올바른 파생 클래스 타입으로 역직렬화할 수 있다.

ConfirmCity<BaseCity>();
//{ "Name":"Seoul","Description":"capital city"}
//result is SerializePolymorphicTypes.Models + BaseCity; // True


ConfirmCity<BigCity>();
//{ "$type":"big","Size":1000,"Name":"Seoul","Description":"capital city"}
//result is SerializePolymorphicTypes.Models + BigCity; // True


ConfirmCity<PopularCity>();
//{ "$type":"popular","SightSeeing":2000,"Name":"Seoul","Description":"capital city"}
//result is SerializePolymorphicTypes.Models + PopularCity; // True

 

 

 

4. 유형 판별자 이름 사용자 정의

default로 생성되는 $type의 이름을 변경할 수 있다.

    [JsonPolymorphic(TypeDiscriminatorPropertyName = "$property")] <----------
    [JsonDerivedType(typeof(BigCity), "big")]
    [JsonDerivedType(typeof(PopularCity), "popular")]

변경한 뒤 확인하면 $type이 아닌 $property로 들어감을 알 수 있다.

{"Name":"Seoul","Description":"capital city"}
result is SerializePolymorphicTypes.Models+BaseCity; // True

{"$property":"big","Size":1000,"Name":"Seoul","Description":"capital city"}
result is SerializePolymorphicTypes.Models+BigCity; // True

{"$property":"popular","SightSeeing":2000,"Name":"Seoul","Description":"capital city"}
result is SerializePolymorphicTypes.Models+PopularCity; // True

 

심지어 $를 제거할 수도 있다.

대소문자도 구분한다.

 

그렇지만, 클래스 계층 구조의 다른 속성 이름과 같으면 충돌난다!!

 

 

5. Unknown Derived Types

다음과 같이 파생클래스를 정의하고 attribute를 생성한 뒤 출력해보면 에러가 발생할 수 있다.

 [JsonDerivedType(typeof(BigCity), "big")] // <--------------- BigAndPopularCity attribute 없음
 public class BaseCity
 {
     public string Name { get; set; } = "Seoul";
     public string? Description { get; set; } = "capital city";
 }

 public class BigCity : BaseCity
 {
     public int Size { get; set; } = 1000;
 }

 public class BigAndPopularCity : BigCity // <--------------------- BaseCity가 아닌 BigCity
 {
     public int SightSeeing { get; set; } = 2000;
 }

 

'Runtime type 'SerializePolymorphicTypes.Models+BigAndPopularCity' is not supported by polymorphic type 'SerializePolymorphicTypes.Models+BaseCity'. Path: $.'

 

이러한 exception에 대한 default 를 변경할 수 있다.

1 ) [JsonPolymorphic( UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]

Base class로 직렬화 되지만, 동일한 클래스로 역직렬화에는 실패함

 

2 ) [JsonPolymorphic( UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToNearestAncestor)]

가장 가까운 class(여기에서는 BigCity)로 직렬화 되지만, 동일한 클래스로 역직렬화에는 실패함

여기에서 FallBackToNearestAncestor를 선택하는데는 모호함의 문제가 있다. 예를 들어서 BigAndPopularCity가 BigCity와 PopularCity 두 개를 상속 받은 경우, Fallback하기 위해 상위 클래스를 선택해야할 때 어떤 기준으로 결정해야할지 모호할 수 있기 때문에 정확하게 역직렬화하기 어렵게된다."다이아몬드 모호함"

 

 

 

6. 다형성 구성 with Contract Model

BaseClass의 Attribute들을 jsonTypeInfo를 덮어씀으로써도 정의할 수 있다.

public class PolymorphicTypeResolver : DefaultJsonTypeInfoResolver
{
    public override JsonTypeInfo GetTypeInfo(Type type, JsonSerializerOptions options)
    {
        JsonTypeInfo jsonTypeInfo = base.GetTypeInfo(type, options);

        Type baseCityType = typeof(BaseCity);
        if (jsonTypeInfo.Type == baseCityType)
        {
            jsonTypeInfo.PolymorphismOptions = new JsonPolymorphismOptions
            {
                TypeDiscriminatorPropertyName = "$city-type",
                IgnoreUnrecognizedTypeDiscriminators = true,
                UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FailSerialization,
                DerivedTypes =
                {
                    new JsonDerivedType(typeof(BigCity), "big"),
                    new JsonDerivedType(typeof(PopularCity), "popular"),
                    new JsonDerivedType(typeof(BigAndPopularCity), "bigpopular")
                }
            };
        }

        return jsonTypeInfo;
    }
}

 

SerializerOption에 해당 Resolver를 넣어준 뒤,

private readonly JsonSerializerOptions options = new (){
    TypeInfoResolver = new PolymorphicTypeResolver(),
    AllowOutOfOrderMetadataProperties = true
};

 

Serialize, Deserialize 할 때 option으로 설정하면 된다.

 var json = JsonSerializer.Serialize(new T(), options);
 BaseCity? result = JsonSerializer.Deserialize<BaseCity>(json, options);

 

728x90
반응형

'Programming > .Net(C#)' 카테고리의 다른 글

Graceful Shutdown 예제(C#)  (0) 2024.01.02
입출력과 변수  (0) 2021.05.10