dotnet 将一个 JSON 字符串嵌入到另一个对象但不被序列化的实现方法

有时候我们会将一段 JSON 字符串存入数据库,以期在某个接口被调用时将其返回给客户端。这种返回一般不是原样返回:我们可能需要对结果包装一下,比如将数据包在 data 字段里同时提供 code 和 message 字段。

代码语言:javascript代码运行次数:0运行复制
{
  "code": 200,
  "message": "OK",
  "data": []
}

简单的办法

这很好办,只要设计一个带泛型参数的 Result 即可:

代码语言:javascript代码运行次数:0运行复制
public class Result<TData>
{
    public int Code { get; set; }
    public string Message { get; set; }
    public TData Data { get; set; }
}

简单的使用方法如下(用 Newtonsoft.Json 处理 JSON 数据):

代码语言:javascript代码运行次数:0运行复制
public class Article
{
    public string Title { get; set; }
    public string Content { get; set; }
}
var json = "从数据库拿到的 JSON 字符串";
var article = JsonConvert.DeserializeObject<Article>(json);
var ret = new Result<Article> { Code = 200, Data = article, Message = "OK" };

以上代码在实际使用中存在几个问题:

  1. 类型必须明确指定。如果存取数据的模型不一致,接口返回的数据可能会丢失字段(也许也是个优点?)。
  2. 不必要的性能损失。因为不会处理数据,反序列化后再序列化就显得很没有必要。

使用 Newtonsoft.Json 中的 JRaw 类型

JRaw 是 Newtonsoft.Json 库中的一种特殊类型,用于表示 JSON 中的原始(raw)数据。它允许你在 JSON 中嵌入原始的 JSON 字符串,而不进行序列化或反序列化。当你使用 JRaw 类型时,类库不会尝试解析该字符串,而是将其保留为原始的 JSON 数据。

改进后的 Result 可以直接将 Data 字段设置为 JRaw 类型:

代码语言:javascript代码运行次数:0运行复制
public class Result
{
    public int Code { get; set; }
    public string Message { get; set; }
    public JRaw Data { get; set; }
}

使用方式:

代码语言:javascript代码运行次数:0运行复制
var json = "{\"Id\":1,\"Name\":\"零五网\",\"Url\":\"\"}";
var ret = new Result { Code = 0, Message = "OK", Data = new JRaw(json) };
Console.WriteLine(JsonConvert.SerializeObject(ret));

输出结果:

代码语言:javascript代码运行次数:0运行复制
{
  "Code": 0,
  "Message": "OK",
  "Data": {"Id":1,"Name":"零五网","Url":";}
}

如果我使用的是 System.Text.Json 怎么办?

System.Text.Json 是由微软官方开发在 .NET Core 3.0 之后引入的库,并在后续版本的 ASP.NET CORE 中默认使用。

虽然在 System.Text.Json 中,没有与 Newtonsoft.Json 中的 JRaw 直接对应的类。但是微软在 .NET 6.0 中引入了 Utf8JsonWriter.WriteRawValue(string json, bool skipInputValidation = false) 方法,因此可以使用一个转换器来实现类似的功能:

代码语言:javascript代码运行次数:0运行复制
/// <summary>
/// 将字符串值的内容序列化为原始 JSON。将验证字符串是否符合 RFC 8259 标准。
/// </summary>
public class RawJsonConverter : JsonConverter<string>
{
    public override string Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        using var doc = JsonDocument.ParseValue(ref reader);
        return doc.RootElement.GetRawText();
    }
    // 是否跳过输入验证,默认为 false
    protected virtual bool SkipInputValidation => false;
    public override void Write(Utf8JsonWriter writer, string value, JsonSerializerOptions options) =>
        // skipInputValidation : true 可提高性能,但仅在确保值表示格式良好的 JSON 时使用!
        writer.WriteRawValue(value, skipInputValidation: SkipInputValidation);
}
/// <summary>
/// 将字符串值的内容序列化为原始 JSON。不验证字符串是否符合 RFC 8259 标准。
/// </summary>
public class UnsafeRawJsonConverter : RawJsonConverter
{
    // 跳过输入验证,默认为 true
    protected override bool SkipInputValidation => true;
}

与之对应的 Result 类型也要进行一些修改,Data 字段需要改为 string 类型,并设置转换器:

代码语言:javascript代码运行次数:0运行复制
public class Result
{
    public int Code { get; set; }
    public string Message { get; set; }
    [JsonConverter(typeof(UnsafeRawJsonConverter))]
    public string Data { get; set; }
}

使用方式:

代码语言:javascript代码运行次数:0运行复制
var json = "{\"Id\":1,\"Name\":\"零五网\",\"Url\":\"\"}";
var ret = new Result { Code = 0, Message = "OK", Data = json };
Console.WriteLine(JsonSerializer.Serialize(ret, new JsonSerializerOptions { WriteIndented = true }));

输出结果:

代码语言:javascript代码运行次数:0运行复制
{<br>  "Code": 0,<br>  "Message": "OK",<br>  "Data": {"Id":1,"Name":"零五网","Url":";}<br>}