我所做的项目是需要调用业务算法的,算法中有一个入参是油品的性质,这个性质有名称、编码、类型等属性,其中类型是固定质量性质、体积性质和其他性质这三种,所以我把其作为枚举类型。问题也由此产生,默认情况下,枚举是以其整数形式进行 JSON 序列化,这就需要同研发算法的同事约定好数值的含义。但是经过协商,算法同事要求我们传递成字符串。因此,我们希望它们在一些情况下以字符串的形式进行序列化。本文将讲解实现这一目标的各种方法。
1、枚举序列化的默认行为
首先,我们有一个性质类:
public class Property
{
public string Code { get; set; }
public string Name { get; set; }
public PropertyType ProType { get; set; }
}
public enum PropertyType
{
Volume,
Weight,
Other
}
我们用 System.Text.Json
中的 Serialize
方法序列化一个 Property
对象:
using System.Text.Json;
var json = JsonSerializer.Serialize(new Property
{
Code = "DEN",
Name = "密度",
ProType = PropertyType.Volume
});
Console.WriteLine(json);
可以看到这个对象序列化为 Json 后的字符串:
{ "Code": "DEN", "Name": "密度", "ProType": 0}
正如我们所看到的,枚举属性的值(PropertyType.Volume
)被序列化为 JSON 字符串后表现为一个整数(0
)。
作为序列化结果 JSON 的消费者,在没有文档的情况下,无法知道这个整数代表的业务含义。
所以,问题来了。如何将枚举属性序列为更直观的字符串形式呢?即,如何将上面示例中的对象序列化为:
{ "Code": "DEN", "Name": "密度", "ProType": "Volume" }
要把枚举序列化为字符串,.Net 标准库 System.Text.Json
和 Newtonsoft 库都为此提供了一个转换器,分别为 JsonStringEnumConverter
和 StringEnumConverter
。下面来看看它们的用法。
2、基于标注的方式
.Net 标准库和 Newtonsoft 库都提供了一个转换器特性 JsonConverterAttribute
,我们可以用它来选择性地标注要序列化为字符串形式的枚举属性或类型。
标注枚举属性
我们可以在枚举属性上使用 JsonConverterAttribute
特性来标注 ProType
属性,并使用 JsonStringEnumConverter
转换器,如下:
using System.Text.Json.Serialization;
public class Property
{
public string Code { get; set; }
public string Name { get; set; }
[JsonConverter(typeof(JsonStringEnumConverter))]
public PropertyType ProType { get; set; }
}
此时序列化的结果为:
{ "Code": "DEN", "Name": "密度", "ProType": "Volume" }
Newtonsoft 库也是类似的,把 JsonStringEnumConverter
替换为 StringEnumConverter
即可。
标注枚举类型
JsonConverterAttribute
特性也可以应用在自定义类型上面,把它应用在 PropertyType
枚举类型上:
using System.Text.Json.Serialization;
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum PropertyType
{
Volume,
Weight,
Other
}
这样,所有的 PropertyType
枚举都会序列化为字符串形式。
3、基于选项的方式
在一些不方便修改类或枚举类型定义的情况下(如调用第三方 SDK),就不能使用标注的方式将枚举序列化为字符串了。.Net 标准库和 Newtonsoft 库还提供了带有转换选项参数的序列化方法,允许我们使用行内(Inline)方式实现这一目标。
.Net 标准库示例如下:
public static string SerializeWithStringEnum(object obj)
{
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonStringEnumConverter());
return JsonSerializer.Serialize(obj, options);
}
而 Newtonsoft 库稍有不同:
public static string SerializeWithStringEnum(object obj)
{
var converter = new StringEnumConverter();
return JsonConvert.SerializeObject(obj, converter);
}
4、全局注册的方式
基于特性标注的方式让我们可以灵活地以可控的方式来操作枚举的序列化。而基于选项的方式允许我们在特定环境下按需处理对象的序列化。但是,有没有办法让所有的枚举在默认情况下被序列化为字符串呢?答案是有的。
对控制台应用程序或 .Net 类库就简单了,直接封装一个通用的序列化方法即可。这里我们主要关心如何在 ASP.NET Core 应用程序中注册全局的序列化规则。
在 ASP.NET Core Web API 应用中,需要在 DI 管道中注册枚举转换器,如下:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers()
.AddJsonOptions(options =>
{
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
var app = builder.Build();
app.MapControllers();
app.Run();
在 ASP.NET Core Minimal API 中,也是需要在 DI 管道中注册枚举转换器,但使用的是 Configure
方式:
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<JsonOptions>(options =>
{
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
var app = builder.Build();
app.MapGet("/", () => new Property
{
Code = "DEN",
Name = "密度",
ProType = PropertyType.Volume
});
app.Run();
如上所述,我们可以通过全局注册的方式来设置应用程序在默认情况下将枚举序列化为字符串,这样可以保持类的干净,也不需要在序列化时指定明确的选项。
5、标志枚举的序列化
标志枚举(bit-mask 枚举)的默认序列化输出也是一个整数,而不是使用标志(Flag)的组合来表示。例如:
using System.Text.Json;
var proTypes = PropertyType.Other | PropertyType.Weight;
var json = JsonSerializer.Serialize(new { ProType = proTypes });
Console.WriteLine(json);
[Flags]
public enum PropertyType
{
Volume = 0,
Weight = 1,
Other = 2
}
序列化结果为:
{ "ProType": 3 }
我们希望输出的是枚举的组合,结果却是一个数字,这对消费者来说很难理解它真正的意义,我们希望它序列化为文件的组合。使用前面的枚举字符串转换器可以解决这个问题,以基于选项的方式为例,示例代码如下:
using System.Text.Json;
using System.Text.Json.Serialization;
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonStringEnumConverter());
var proTypes = PropertyType.Other | PropertyType.Weight;
var json = JsonSerializer.Serialize(new { ProType = proTypes }, options);
Console.WriteLine(json);
{ "ProType": "Other, Weight" }
这样就得到我们想要的文本组合序列化结果了。
6、自定义枚举字符串
在将枚举序列化为字符串时,我们可能希望进行一些微调。例如,转换为 camelCase 或其它更有意义的文本形式。
序列化为 camelCase 形式
JsonStringEnumConverter
有一个接收命名策略的重载构造函数,给其传入 JsonNamingPolicy.CamelCase
参数即可将枚举序列为 camelCase 文本形式,例如:
var options = new JsonSerializerOptions();
options.Converters.Add(new JsonStringEnumConverter(JsonNamingPolicy.CamelCase));
var json = JsonSerializer.Serialize(new Property
{
Code = "DEN",
Name = "密度",
ProType = PropertyType.Volume
});
Console.WriteLine(json);
序列化结果为:
{ "Code": "DEN", "Name": "密度", "ProType": "volume" }
序列化为自定义文本
由于语言中的变量命名规则,枚举成员在以常规方式序列化时并不总是能传达有意义的文本。为了解决这个问题,我们可以使用 EnumMemberAttribute
特性,它是专门为此目的引入的。我们来定义一个新的枚举:
public enum PropertyType
{
[EnumMember(Value = "体积性质")]
volume,
[EnumMember(Value = "质量性质")]
Weight,
[EnumMember(Value = "其他性质")]
Other,
}
我们重新定义了一个 PropertyType
枚举,在其成员上标注 EnumMember
特性,以提供更有意义的文本表达。
Newtonsoft 库是支持的,序列化后,我们可以看到它输出了我们想要的文本。
注:虽然 Donet 标准库原生暂不支持将枚举序列化为自定义的文本,但依然可以通过自定义转换器来实现。
7、自定义转换器
当接收到的Json格式字符串与本地已有类型不统一时,需要进行自定义的反序列化过程,反之亦然,例如Json字符串中以字符串"TRUE"表示布尔类型true(不自定义,这个过程依然走的通,只是以此举例),以字符串"FALSE"表示布尔类型false时,需要自定义如下:
/// <summary>
/// 自定义布尔类型数据转换规则
/// </summary>
public class MyBoolConverter : JsonConverter{
private const string TrueStr = "TRUE";
private const string FalseStr = "FALSE";
public override bool CanConvert(Type objectType) => true;
//反序列化
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
if (reader.ValueType == typeof(string))
{
if ((string)reader.Value == TrueStr)
{
return true;
}
else {
return false;
}
}
return false;
}
//序列化
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) {
if (value.GetType() == typeof(bool))
{
bool result = (bool)value;
if (result)
{
writer.WriteValue(TrueStr);
}
else {
writer.WriteValue(FalseStr);
}
}
}
}
然后,在需要操作的类型定义中的字段/属性中加入该特性:
private class MyClass{
[JsonConverter(typeof(MyBoolConverter))]
public bool MyBool;
}
此时
string jsonStr = @"{""MyBool"": ""TRUE""}";
MyClass1 myClass = JsonConvert.DeserializeObject<MyClass1>(jsonStr);
Console.WriteLine(myClass.MyBool); //True
Console.WriteLine(JsonConvert.SerializeObject(myClass)); //{"MyBool":"TRUE"}
8、总结
枚举默认是以其整数形式进行 JSON 序列化的,这要求消费者事先了解这些数字的实际含义,给代码的理解也带来了一定的困难。所以 .Net 基础库和流行的 Newtonsoft 库都提供了将枚举序列化为字符串的多种方法。根据需求,可以选择使用本文讲的基于特性标注的方式、基于选项参数的方式和全局方式。
另外,除了可以将枚举序列化为成员名字符串,还可以自定义文本的输出,如输出为 camelCase 文本形式和自定义输出的文本内容等。