Change property names conditionally during serialization

103 Views Asked by At

I have model classes ClassA and ClassB.

These classes are returned as result from two controllers: XController and YController.

When returning these classes as result from YController, default serialization is to be used.

When returning these classes as result from XController, two properties of ClassA should be renamed (no change to the types or how the values are output) and one property of ClassB needs to be renamed (same as before). XController only has GET calls, so doesn't need to handle custom de-serialization. Using JsonPropertyName doesn't meet this requirement.

How can I do this?

I followed Using multiple JSON serialization settings in ASP.NET Core to add a custom SystemTextJsonOutputFormatter (called ExternalJsonOutputFormatter (for outputting JSON to be used by "external" clients)). However, I can't see where and how I can customize the names of specific properties. My guess is I have to override WriteAsync(...) method, but don't know what to do in there (e.g., how low-level do I have to go to achieve what I want?).

Even links to online examples could be handy (I couldn't find anything that would show me what I need).

I would highly prefer to stick to System.Text.Json.

2

There are 2 best solutions below

0
markvgti On

As a placeholder I've implemented the following solution (since I needed a solution fast), though I'd prefer a better, more standardized way of doing this, so looking forward to the answers.

I already had a utility method that takes an object and converts it into an equivalent IDictionary<string, object?>:

public static IDictionary<string, object?> ToDictionary(object obj,
        ISet<string>? propertiesToIgnore = null)

When returned as a result of an endpoint call, the serialization is exactly as expected (think about it: the simplest & most direct representation of JSON in C# is IDictionary<string, object?>, possibly contained within an IList).

I created an attribute DictionaryPropertyNameAttribute (analogous to JsonPropertyNameAttribute) which can be used to supply custom names. The difference is, while JsonPropertyNameAttribute, once set, will always be applied, I added a bool to my ToDictionary call to allow the method caller to turn on/off the renaming:

    public static IDictionary<string, object?> ToDictionary(object obj,
        ISet<string>? propertiesToIgnore = null, bool useCustomNames = false)

The useCustomNames is false by default for backwards compatibility.

This solution works, though, as mentioned previously, I'm open to other, better ideas.

0
dbc On

The article to which you linked, Using multiple JSON serialization settings in ASP.NET Core by Thomas Levesque, allows you to introduce "named" serializer options, then apply them to selected controllers via a custom attribute that specifies the option's name.

Assuming you implemented the code from that article, if you modify XController as follows:

public static partial class JsonExtensions
{
    public const string XOptionsName = "XOptions";
}

[JsonSettingsName(JsonExtensions.XOptionsName )]
public class XController : ControllerBase
{
    [HttpGet("GetClassA")]
    public ClassA GetClassA() { return new ClassA { }; }

    [HttpGet("GetClassB")]
    public ClassB GetClassB() { return new ClassB { }; }
}

You should be able to apply custom JsonSerializerOptions to any controller marked with [JsonSettingsName(JsonExtensions.XOptionsName )] as follows:

builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
        // Add your default options here
    })
    .AddJsonOptions(JsonOptions.XOptionsName, options =>
    {
        // Add the options required by XController here.
    });

Having done that, you can add in attribute-based alternate JSON property names. First introduce the following custom attribute and typeInfo modifier

public sealed class JsonAlternatePropertyNameAttribute : JsonAttribute
{
    public JsonAlternatePropertyNameAttribute(string name, string settingsName) => (this.Name, this.SettingsName) = (name, settingsName);
    public string Name { get; }
    public string SettingsName { get; }
}

public static partial class JsonExtensions
{
    public static Action<JsonTypeInfo> UseAlternateNames(string settingsName) => typeInfo =>
    {
        if (typeInfo.Kind != JsonTypeInfoKind.Object)
            return;
        foreach (var property in typeInfo.Properties)
        {
            if (property.AttributeProvider?.GetCustomAttributes(typeof(JsonAlternatePropertyNameAttribute), true) is {} list)
                foreach (JsonAlternatePropertyNameAttribute attr in list)
                    if (attr.SettingsName == settingsName)
                        property.Name = attr.Name;
        }
    };
}

Next, modify ClassA and ClassB by adding JsonAlternatePropertyNameAttribute to the properties that should be conditionally renamed when serializing with the "XOptions" settings:

public class ClassA
{
    public string SomeOtherProperty { get; set; } = ""; // Roughly 30 of these
    
    [JsonAlternatePropertyName("AlternateName1", JsonExtensions.XOptionsName)]
    public string Property1 { get; set; }
    [JsonAlternatePropertyName("AlternateName2", JsonExtensions.XOptionsName)]
    public string Property2 { get; set; }
}

public class ClassB
{
    public string SomeOtherProperty { get; set; } = ""; // Roughly 30 of these

    [JsonAlternatePropertyName("AlternateName3", JsonExtensions.XOptionsName)]
    public string Property3{ get; set; }
    [JsonAlternatePropertyName("AlternateName4", JsonExtensions.XOptionsName)]
    public string Property4 { get; set; }
}

Finally, update your builder code to invoke the typeInfo modifier for controllers marked with [JsonSettingsName(JsonExtensions.XOptionsName )]:

builder.Services.AddControllers()
    .AddJsonOptions(options =>
    {
        // Add your default options here
    })
    .AddJsonOptions(/*JsonExtensions.XOptionsName,*/ options =>
    {
        // Add the options required by XController here.
        options.JsonSerializerOptions.TypeInfoResolver = (options.JsonSerializerOptions.TypeInfoResolver ?? new DefaultJsonTypeInfoResolver())
            .WithAddedModifier(JsonExtensions.UseAlternateNames(JsonExtensions.XOptionsName));
        // Add anything else you want here, e.g.
        options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
    });

With all that done, the relevant properties of ClassA and ClassB should be renamed when returned from controllers (or methods) marked with Thomas Levesque's attribute, and not otherwise.