공식문서를 옮기며 학습한 내용입니다. (.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; }
}
이 시나리오에서 WeatherForecastWithCity를 WeatherForecastBase로 역직렬화하면 런타임 타입이 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);
'Programming > .Net(C#)' 카테고리의 다른 글
Graceful Shutdown 예제(C#) (0) | 2024.01.02 |
---|---|
입출력과 변수 (0) | 2021.05.10 |