-1

I recently came accross this problem. We are using a external API that returns records in a JSON format. But all the keys inside the JSON objects are in a special string.

For example, the JSON would look like this:

{
    "fieldA_0001": "value",
    "fieldA_0002": "value",
    "fieldA_0003": "value" 
}

Now in our code the class models are setup like this:

public class Project {
    public string ProjectId { get; set; }

    [JsonPropertyName=("fieldA_0001")]
    public string ProjectName { get; set; }

    [JsonPropertyName=("fieldA_0002")]
    public string ProjectDescription { get; set; }

    [JsonPropertyName=("fieldA_0003")]
    public string ProjectCreator { get; set; }
}

Project.cs

We want to have some keys and values in our appsettings.json that override these hardcoded property names. Like this:

{
    "fieldConfig": {
        "projectNameField": "fieldA_0001",
        "projectDescriptionField": "fieldA_0002",
        "projectCreatorField": "fieldA_0003"
    }
}

appsettings.json

public class Project {
    public string ProjectId { get; set; }

    [JsonPropertyName=(config.GetSection("fieldConfig.projectNameField"))]
    public string ProjectName { get; set; }

    [JsonPropertyName=(config.GetSection("fieldConfig.projectDescriptionField"))]
    public string ProjectDescription { get; set; }

    [JsonPropertyName=(config.GetSection("fieldConfig.projectCreatorField"))]
    public string ProjectCreator { get; set; }
}

Project.cs

We want it to be like this whenever the JSON keys are changing because in the event of a project switch all the keys change and result in the external API serving different records.

Is this possible and how would it be possible?

dbc
  • 104,963
  • 20
  • 228
  • 340
  • Pls show the code you have tried so far and what is the problem, what is not working. You can't just post your fantasy as a question. You should know that property attributes can be only created by a compiler and can not be chanaged duing the runtime – Serge Jun 09 '23 at 16:04
  • Arguments you pass to attributes have to have to be constant values as these are locked in at compile time. If you want to base it on a config file, you'll have to look into a different approach. – ProgrammingLlama Jun 09 '23 at 16:13
  • You are applying [`JsonPropertyNameAttribute`](https://learn.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonpropertynameattribute) from System.Text.Json to your model, but you have tagged this Json.NET. Which JSON serializer are you using? It's possible to achieve what you want with System.Text.Json in .NET 7 or later, or Json.NET in any version. – dbc Jun 17 '23 at 16:41

1 Answers1

0

There is no built-in way to configure property names from injected IConfiguration values at runtime. If you need this, you will have to implemented it yourself using contract customization in Json.NET (any version) or System.Text.Json (in .NET 7 and later). Since you tagged your question but used the attribute [JsonPropertyName] from System.Text.Json I'll give answers for both.

Whichever serializer you use, first create a custom attribute for the configuration key you want to use for a given property:

[System.AttributeUsage(System.AttributeTargets.Property | System.AttributeTargets.Field, AllowMultiple = false, Inherited = true)]
public sealed class JsonConfiguredPropertyNameAttribute : System.Attribute
{
    public string Key { get; }
    public JsonConfiguredPropertyNameAttribute(string key) => this.Key = key;
}

Next, apply it to your class as follows:

public class Project {
    public string ProjectId { get; set; }

    [JsonConfiguredPropertyName("fieldConfig:projectNameField")]
    public string ProjectName { get; set; }

    [JsonConfiguredPropertyName("fieldConfig:projectDescriptionField")]
    public string ProjectDescription { get; set; }

    [JsonConfiguredPropertyName("fieldConfig:projectCreatorField")]
    public string ProjectCreator { get; set; }
}

Note that, as explained in Configuration keys and values, hierarchical keys should be referred to using a colon separator ::

"fieldConfig:projectNameField"

If you use Json.NET, create the following custom contract resolver:

public class PropertyNamesFromConfigurationContractResolver : DefaultContractResolver
{
    readonly IConfiguration config;
    
    public PropertyNamesFromConfigurationContractResolver(IConfiguration config) => this.config = config ?? throw new ArgumentNullException(nameof(config));

    protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
    {
        var property = base.CreateProperty(member, memberSerialization);
        if (member.GetCustomAttribute<JsonConfiguredPropertyNameAttribute>()?.Key is {} key)
            property.PropertyName = config[key] ?? property.PropertyName;
        return property;
    }   
}

Then, configure your JsonSerializerSettings using some injected IConfiguration as follows:

var settings = new JsonSerializerSettings();
settings.ContractResolver = new PropertyNamesFromConfigurationContractResolver(config)
{
    // Add other properties as required, e.g.
    NamingStrategy = new CamelCaseNamingStrategy(),
};

Note that Json.NET's contract resolver caches contracts, so if your configuration changes you will have to construct a new instance of PropertyNamesFromConfigurationContractResolver.

Demo fiddle #1 here.

If you use System.Text.Json, then in .NET 7 and later you can use a typeInfo modifier to customize your type's contract in a similar manner.

First, create the modifier:

public class JsonExtensions
{
    public static Action<JsonTypeInfo> InitializePropertyNamesFromConfiguration(IConfiguration config) => 
        typeInfo => 
        {
            if (typeInfo.Kind == JsonTypeInfoKind.Object)
                foreach (var property in typeInfo.Properties)
                    if ((property.AttributeProvider?
                        .GetCustomAttributes(typeof(JsonConfiguredPropertyNameAttribute), true)
                        .Cast<JsonConfiguredPropertyNameAttribute>()
                        .SingleOrDefault()?.Key) is {} key)
                        property.Name = config[key] ?? property.Name;
        };
}

Then when setting up your JsonSerializerOptions apply the modifer as follows, once again making use of some injected IConfiguration:

var options = new JsonSerializerOptions();
options.TypeInfoResolver = new DefaultJsonTypeInfoResolver
{
    Modifiers = { JsonExtensions.InitializePropertyNamesFromConfiguration(config) },
};
// Add other options as required, e.g.
options.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;

Notes:

  • There is no way to customize System.Text.Json contracts in .NET versions earlier than .NET 7.

  • System.Text.Json is case sensitive (unlike Json.NET) so the field names will have to match exactly unless you set JsonSerializerOptions.PropertyNameCaseInsensitive = true.

  • I believe that DefaultJsonTypeInfoResolver also caches contracts, so if your configuration changes you may need to replace it with a new instance.

Demo fiddle #2 here.

dbc
  • 104,963
  • 20
  • 228
  • 340