System.Text.Json: 如何将对象序列化为带键的嵌套子对象?

System.Text.Json: How do I serialize an object to be a nested subobject with a key?

我有一个以下 class 的对象我想序列化:

[Serializable]
public class BuildInfo
{
    public DateTime BuildDate { get; set; }

    public string BuildVersion { get; set; }
}

我编写了以下方法来序列化任何对象:

...
public static string JsonSerialize<TValue>(TValue value)
{
    var options = new JsonSerializerOptions
    {

        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        WriteIndented = true
    };

    return JsonSerializer.Serialize(value, options);
}
...

我得到以下输出(示例):

{
    "buildDate": "2021-04-22T17:29:59.1611109+03:00",
    "buildVersion": "1.0.1"
}

我想得到这样的输出:

{
    "buildInfo": {
        "buildDate": "2021-04-22T17:29:59.1611109+03:00",
        "buildVersion": "1.0.1"
    }
}

我该怎么做?

你可以试试这个

JsonSerialize( new { BuildInfo = build })

编辑

处理此问题的简单方法是在调用 JsonSerialize

之前有一个包装器 class
var buildInfoWrapper = new { BuildInfo = buildInfo };
JsonSerialize(buildInfoWrapper)

我花了半天时间,但我做到了。

首先,我们需要定义自定义JSON转换器:

public class TopAsNestedJsonConverter<TValue> : JsonConverter<TValue>
{
    private readonly string _propertyName;

    private readonly JsonSerializerOptions _referenceOptions;

    public TopAsNestedJsonConverter(JsonSerializerOptions referenceOptions)
    {
        _propertyName = FormatPropertyName(typeof(TValue).Name, referenceOptions);
        _referenceOptions = referenceOptions;
    }

    public override TValue? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        var value = default(TValue);
        var propertyFound = false;

        if (reader.TokenType != JsonTokenType.StartObject)
        {
            return value;
        }

        while (reader.Read())
        {
            switch (reader.TokenType)
            {
                case JsonTokenType.PropertyName when IsPropertyFound(reader.GetString()):
                    propertyFound = true;
                    break;
                case JsonTokenType.StartObject when propertyFound:
                    value = JsonSerializer.Deserialize<TValue>(ref reader, _referenceOptions);
                    propertyFound = false;
                    break;
            }
        }

        return value;
    }

    public override void Write(Utf8JsonWriter writer, TValue? value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();
        writer.WritePropertyName(_propertyName);
        JsonSerializer.Serialize(writer, value, _referenceOptions);
        writer.WriteEndObject();
    }

    private bool IsPropertyFound(string? propertyName)
    {
        return string.Equals(_propertyName, propertyName,
            _referenceOptions.PropertyNameCaseInsensitive
                ? StringComparison.OrdinalIgnoreCase
                : StringComparison.Ordinal);
    }

    private static string FormatPropertyName(string propertyName, JsonSerializerOptions options)
    {
        return options.PropertyNamingPolicy != null
            ? options.PropertyNamingPolicy.ConvertName(propertyName)
            : propertyName;
    }
}

然后,我们需要一个实用程序class以便于使用:

public static class JsonUtilities
{
    public static string JsonSerializeTopAsNested<TValue>(TValue? value, JsonSerializerOptions? options = null)
    {
        return JsonSerializer.Serialize(value, GetConverterOptions<TValue>(options));
    }

    public static async Task<string> JsonSerializeTopAsNestedAsync<TValue>(TValue? value,
        JsonSerializerOptions? options = null, CancellationToken cancellationToken = default)
    {
        await using var stream = new MemoryStream();
        await JsonSerializer.SerializeAsync(stream, value, GetConverterOptions<TValue>(options), cancellationToken);
        return Encoding.UTF8.GetString(stream.ToArray());
    }

    public static TValue? JsonDeserializeTopAsNested<TValue>(string json, JsonSerializerOptions? options)
    {
        return JsonSerializer.Deserialize<TValue>(json, GetConverterOptions<TValue>(options));
    }

    public static async Task<TValue?> JsonDeserializeTopAsNestedAsync<TValue>(string json,
        JsonSerializerOptions? options, CancellationToken cancellationToken = default)
    {
        await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json));
        return await JsonSerializer.DeserializeAsync<TValue>(stream, GetConverterOptions<TValue>(options),
            cancellationToken);
    }

    private static JsonSerializerOptions GetConverterOptions<TValue>(JsonSerializerOptions? options = null)
    {
        var referenceOptions = options == null ? new JsonSerializerOptions() : new JsonSerializerOptions(options);
        var converterOptions = new JsonSerializerOptions(referenceOptions);
        converterOptions.Converters.Add(new TopAsNestedJsonConverter<TValue>(referenceOptions));

        return converterOptions;
    }
}

最后,示例代码:

public class BuildInfo
{
    public DateTime? BuildDate { get; init; }

    public string? BuildVersion { get; init; }
}

public static class Program
{
    public static async Task Main()
    {
        var buildInfo = new BuildInfo
        {
            BuildDate = DateTime.Now,
            BuildVersion = "1.0.1"
        };

        var options = new JsonSerializerOptions(JsonSerializerDefaults.Web)
        {
            PropertyNameCaseInsensitive = false,
            WriteIndented = true
        };

        var json = await JsonUtilities.JsonSerializeTopAsNestedAsync(buildInfo, options);

        Console.WriteLine(json);

        var info = await JsonUtilities.JsonDeserializeTopAsNestedAsync<BuildInfo>(json, options);

        Console.WriteLine($"{info?.BuildDate} {info?.BuildVersion}");
    }
}

如果您发现代码或算法本身有任何错误,我将很高兴。

考虑到所有的答案和评论,我得到以下代码:

public static class JsonUtilities
{
    public static string JsonSerializeTopAsNested<TValue>(TValue? value, JsonSerializerOptions? options = null)
    {
        return JsonSerializer.Serialize(ToSerializableObject(value, options), options);
    }

    public static async Task<string> JsonSerializeTopAsNestedAsync<TValue>(TValue? value,
        JsonSerializerOptions? options = null, CancellationToken cancellationToken = default)
    {
        await using var stream = new MemoryStream();
        await JsonSerializer.SerializeAsync(stream, ToSerializableObject(value, options), options,
            cancellationToken);

        return Encoding.UTF8.GetString(stream.ToArray());
    }

    public static TValue? JsonDeserializeTopAsNested<TValue>(string json, JsonSerializerOptions? options = null)
    {
        var serializableObject = JsonSerializer.Deserialize<Dictionary<string, TValue?>>(json, options);

        return FromSerializableObject(serializableObject, options);
    }

    public static async Task<TValue?> JsonDeserializeTopAsNestedAsync<TValue>(string json,
        JsonSerializerOptions? options = null, CancellationToken cancellationToken = default)
    {
        await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(json));
        var serializableObject = await JsonSerializer.DeserializeAsync<Dictionary<string, TValue?>>(stream, options,
            cancellationToken);

        return FromSerializableObject(serializableObject, options);
    }

    private static Dictionary<string, TValue?> ToSerializableObject<TValue>(TValue? propertyValue,
        JsonSerializerOptions? options)
    {
        var propertyName = GetPropertyName<TValue>(options);

        return propertyValue == null && options is {IgnoreNullValues: true}
            ? new Dictionary<string, TValue?>()
            : new Dictionary<string, TValue?> {[propertyName] = propertyValue};
    }

    private static TValue? FromSerializableObject<TValue>(IReadOnlyDictionary<string, TValue?>? serializableObject,
        JsonSerializerOptions? options)
    {
        var propertyName = GetPropertyName<TValue>(options);

        return serializableObject != null && serializableObject.ContainsKey(propertyName)
            ? serializableObject[propertyName]
            : default;
    }

    private static string GetPropertyName<TValue>(JsonSerializerOptions? options)
    {
        var propertyName = typeof(TValue).Name;
        return options?.PropertyNamingPolicy?.ConvertName(propertyName) ?? propertyName;
    }
}

如果发现更多评论和错误,我将不胜感激。