[Dotnet] JsonStringEnumConverter 的應用

最近在寫需求時,前端回報 API 的某個欄位不是應該數字 enum 嗎,怎麼變成文字,檢查以後才發現,前人使用 JsonStringEnumConverter 這個功能將 enum 全部轉成 string 輸出給前端。

為了要讓舊有 API 維持,新的 API 也可以照所希望的輸出成數字,而去找了一些解法。

統一輸出格式

如果想要全站的 Enum 都輸出的時候都是 string 的格式,可以在 JsonSerializerOptions 加上 JsonStringEnumConverter 設定。

1
2
3
4
5
6
7
8
9
// controller 寫法
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});

// minial api 寫法
builder.Services.Configure<JsonOptions>(options => options.SerializerOptions.Converters.Add(new JsonStringEnumConverter()));

這邊要注意,要根據你的寫法來使用不同的設定方法,給 minial api 的寫法沒有相容 controller 的設定。

Ignore 部分 Enum

前面的做法是統一輸出格式都是 string,如果今天有一個新的 enum 並不希望被輸出成 string,那可以怎麼辦?

首先要搞懂 JsonStringEnumConverter 到底做了些什麼,根據 source code 可以看到 CanConvert 這個方法內做了 IsEnum 的判斷

JsonStringEnumConverter.cslink
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
/// <summary>
/// Converter to convert enums to and from strings.
/// </summary>
/// <remarks>
/// Reading is case insensitive, writing can be customized via a <see cref="JsonNamingPolicy" />.
/// </remarks>
[RequiresDynamicCode(
"JsonStringEnumConverter cannot be statically analyzed and requires runtime code generation. " +
"Applications should use the generic JsonStringEnumConverter<TEnum> instead.")]
public class JsonStringEnumConverter : JsonConverterFactory
{
private readonly JsonNamingPolicy? _namingPolicy;
private readonly EnumConverterOptions _converterOptions;

/// <summary>
/// Constructor. Creates the <see cref="JsonStringEnumConverter"/> with the
/// default naming policy and allows integer values.
/// </summary>
public JsonStringEnumConverter() : this(namingPolicy: null, allowIntegerValues: true)
{
// An empty constructor is needed for construction via attributes
}

/// <summary>
/// Constructor.
/// </summary>
/// <param name="namingPolicy">
/// Optional naming policy for writing enum values.
/// </param>
/// <param name="allowIntegerValues">
/// True to allow undefined enum values. When true, if an enum value isn't
/// defined it will output as a number rather than a string.
/// </param>
public JsonStringEnumConverter(JsonNamingPolicy? namingPolicy = null, bool allowIntegerValues = true)
{
_namingPolicy = namingPolicy;
_converterOptions = allowIntegerValues
? EnumConverterOptions.AllowNumbers | EnumConverterOptions.AllowStrings
: EnumConverterOptions.AllowStrings;
}

/// <inheritdoc />
public sealed override bool CanConvert(Type typeToConvert)
{
return typeToConvert.IsEnum;
}

/// <inheritdoc />
public sealed override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
if (!typeToConvert.IsEnum)
{
ThrowHelper.ThrowArgumentOutOfRangeException_JsonConverterFactory_TypeNotSupported(typeToConvert);
}

return EnumConverterFactory.Create(typeToConvert, _converterOptions, _namingPolicy, options);
}
}

根據這個做法,可以來客製一個 Converter 讓指定的 enum 可以忽略這個規則

參考自 stackoverflow,這個做法可以忽略多種 Converter

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class IgnoreJsonConverterFactory : JsonConverterFactory
{
private readonly JsonConverterFactory _innerFactory;
readonly HashSet<Type> optOutTypes;

public IgnoreJsonConverterFactory(JsonConverterFactory innerFactory, params Type [] optOutTypes)
{
_innerFactory = innerFactory;
this.optOutTypes = optOutTypes.ToHashSet();
}

public override bool CanConvert(Type typeToConvert)
{
return _innerFactory.CanConvert(typeToConvert) && !optOutTypes.Contains(typeToConvert);
}
public override JsonConverter CreateConverter(Type typeToConvert, JsonSerializerOptions options)
{
return _innerFactory.CreateConverter(typeToConvert, options);
}
}

在 Converter 設定時就可以指定要忽略規則的 enum

1
2
3
options.SerializerOptions.Converters.Add(new OptOutJsonConverterFactory(new JsonStringEnumConverter(),
typeof(DayOfWeek)
));

這個做法可以優化成使用 attribute

指定 Enum 輸出

當 Enum 都是自己定義,那也可以換個思維,指定哪些 enum 輸出成 string,JsonConverter 本身可以當作一個 attribute 來使用
有設定這些 Attribute 的在輸出時,都會根據設定來做轉換,連 service 都不用設定,更加優美

How to customize property names and values with System.Text.Json - .NET | Microsoft Learn

1
2
3
4
5
[JsonConverter(typeof(JsonStringEnumConverter<Precipitation>))]
public enum Precipitation
{
Drizzle, Rain, Sleet, Hail, Snow
}