andreilomakin
andreilomakin

Reputation: 419

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

I have an object of the following class that I want to serialize:

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

    public string BuildVersion { get; set; }
}

I wrote the following method to serialize any objects:

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

        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        WriteIndented = true
    };

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

I got the following output (example):

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

I want to get output like this:

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

How can I do it?

Upvotes: 1

Views: 3007

Answers (5)

karwenzman
karwenzman

Reputation: 47

After additional research I found this solution. The key is the expression new { buildInfo = objectToSave}. buildInfo can be any string. The quotation marks are not needed.

var objectToSave = new BuildInfo();
var options = new JsonSerializerOptions { WriteIndented = true };
var jsonString = JsonSerializer.Serialize(new { buildInfo = objectToSave}, options);
File.WriteAllText(...your path..., jsonString);

then the file will look like this:

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

Upvotes: 0

After_Sunset
After_Sunset

Reputation: 714

The solution you ended up with is nice in some regards but in general I think its way over engineered. There is a much simpler way to do this that handles what you want to achieve automatically. You shouldn't have to call any utilities. It looks like the Read method logic in your converter can be completely removed, anything your doing there can just be handled OOTB with attributes.

internal sealed class BuildInfoConverter : JsonConverter<BuildInfo>
{
    public override BuildInfo? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
    {
        //Only implement if needed
        return null;
    }

    public override void Write(Utf8JsonWriter writer, BuildInfo value, JsonSerializerOptions options)
    {
        writer.WriteStartObject();
        writer.WritePropertyName("buildInfo");
        writer.WriteStartObject();
        
        JsonSerializer.Serialize(writer, value, options);                       
       
        writer.WriteEndObject();
        writer.WriteEndObject();
    }
}

Then just add [JsonConverter(typeof(BuildInfoConverter))] as an attribute above BuildInfo class. It seems that this solution is drastically better and it can be tweaked to do any kind of serialization/deserialization that you want.

EDIT left out one minor but important step. You have to add the converter to your serializer options like so.

    protected JsonSerializerOptions serializeOptions = new JsonSerializerOptions
    {
        //Options here
    };
    serializeOptions.Converters.Add(new BuildInfoConverter());

Now you can serialize like so

   JsonSerializer.Serialize(buildInfo,typeof(BuildInfo),serializeOptions);

Upvotes: 2

andreilomakin
andreilomakin

Reputation: 419

Taking all the answers and comments into account, I got the following code:

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;
    }
}

I would appreciate additional comments and bugs found.

Upvotes: 0

andreilomakin
andreilomakin

Reputation: 419

It took me half a day, but I did it.

First, we need to define custom JSON converter:

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;
    }
}

Then, we need an utility class for easy use:

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;
    }
}

Finally, the example code:

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}");
    }
}

I will be glad if you find any errors in the code or in the algorithm itself.

Upvotes: 0

Krishna Varma
Krishna Varma

Reputation: 4260

you can try this

JsonSerialize( new { BuildInfo = build })

EDIT

The simple way to handle this to have a wrapper class before calling the JsonSerialize

var buildInfoWrapper = new { BuildInfo = buildInfo };
JsonSerialize(buildInfoWrapper)

Upvotes: 2

Related Questions