rbr94
rbr94

Reputation: 2287

Fail to serialize IConfigurationSection from Json

I have following Json-based configuration file:

{
  "PostProcessing": {
    "ValidationHandlerConfiguration": {
      "MinimumTrustLevel": 80,
      "MinimumMatchingTrustLevel": 75
    },
    "MatchingCharacterRemovals": [
      "-",
      "''",
      ":"
    ]
  },
  "Processing": {
    "OrderSelection": {
      "SelectionDaysInterval": 30,
      "SelectionDaysMaximum": 365
    }
  }
}

As serialization framework I use Newtonsoft. To serialize this config into objects I have implemented following classes:

[JsonObject(MemberSerialization.OptIn)]
public class RecognitionConfiguration {
    [JsonProperty(PropertyName = "PostProcessing", Required = Required.Always)]
    public PostRecognitionConfiguration PostRecognitionConfiguration { get; set; }

    [JsonProperty(PropertyName = "Processing", Required = Required.Always)]
    public ProcessRecognitionConfiguration ProcessRecognitionConfiguration { get; set; }
}

[JsonObject(MemberSerialization.OptIn)]
public class PostRecognitionConfiguration {
    [JsonProperty(Required = Required.Always)]
    public ValidationHandlerConfiguration ValidationHandlerConfiguration { get; set; }

    [JsonProperty] public List<string> MatchingCharacterRemovals { get; set; }
}

[JsonObject(MemberSerialization.OptIn)]
public class ProcessRecognitionConfiguration {
    [JsonProperty(PropertyName = "OrderSelection", Required = Required.Always)]
    public OrderSelectionConfiguration OrderSelectionConfiguration { get; set; }
}

In a class I try to serialize a specific configuration section into these class structures using IConfigurationSection.Get().

var serializedConfiguration = this.ConfigurationSection.Get<RecognitionConfiguration>();

But when I debug the code, I always get an "empty" variable serializedConfiguration which is not null, but all properties are null.

enter image description here

If I use

this.ConfigurationSection.GetSection("Processing").Get<ProcessRecognitionConfiguration>()

or change the naming of the properties in the json file to exactly match the property names in the classes like this:

{   
  "ProcessRecognitionConfiguration": {
    "OrderSelectionConfiguration": {
      "SelectionDaysInterval": 30,
      "SelectionDaysMaximum": 365
    }
  }
}

it it works fine. Do you have any idea, why setting PropertyName on JsonProperty does not seem to have any effect?

Upvotes: 6

Views: 2180

Answers (3)

Luke Vo
Luke Vo

Reputation: 20718

EDIT: I found this one is much more pleasant than my previous solutions: Bind everything in an ExpandoObject, write them to JSON and use JSON.NET to bind them back. Using the code of this article:

namespace Microsoft.Extensions.Configuration
{

    public static class ConfigurationBinder
    {

        public static void BindJsonNet(this IConfiguration config, object instance)
        {
            var obj = BindToExpandoObject(config);

            var jsonText = JsonConvert.SerializeObject(obj);
            JsonConvert.PopulateObject(jsonText, instance);
        }

        private static ExpandoObject BindToExpandoObject(IConfiguration config)
        {
            var result = new ExpandoObject();

            // retrieve all keys from your settings
            var configs = config.AsEnumerable();
            foreach (var kvp in configs)
            {
                var parent = result as IDictionary<string, object>;
                var path = kvp.Key.Split(':');

                // create or retrieve the hierarchy (keep last path item for later)
                var i = 0;
                for (i = 0; i < path.Length - 1; i++)
                {
                    if (!parent.ContainsKey(path[i]))
                    {
                        parent.Add(path[i], new ExpandoObject());
                    }

                    parent = parent[path[i]] as IDictionary<string, object>;
                }

                if (kvp.Value == null)
                    continue;

                // add the value to the parent
                // note: in case of an array, key will be an integer and will be dealt with later
                var key = path[i];
                parent.Add(key, kvp.Value);
            }

            // at this stage, all arrays are seen as dictionaries with integer keys
            ReplaceWithArray(null, null, result);

            return result;
        }

        private static void ReplaceWithArray(ExpandoObject parent, string key, ExpandoObject input)
        {
            if (input == null)
                return;

            var dict = input as IDictionary<string, object>;
            var keys = dict.Keys.ToArray();

            // it's an array if all keys are integers
            if (keys.All(k => int.TryParse(k, out var dummy)))
            {
                var array = new object[keys.Length];
                foreach (var kvp in dict)
                {
                    array[int.Parse(kvp.Key)] = kvp.Value;
                }

                var parentDict = parent as IDictionary<string, object>;
                parentDict.Remove(key);
                parentDict.Add(key, array);
            }
            else
            {
                foreach (var childKey in dict.Keys.ToList())
                {
                    ReplaceWithArray(input, childKey, dict[childKey] as ExpandoObject);
                }
            }
        }

    }
}

Usage:

        var settings = new MySettings();
        this.Configuration.BindJsonNet(settings);

Here is my testing MySettings class:

public class MySettings
{

    [JsonProperty("PostProcessing")]
    public SomeNameElseSettings SomenameElse { get; set; }

    public class SomeNameElseSettings
    {

        [JsonProperty("ValidationHandlerConfiguration")]
        public ValidationHandlerConfigurationSettings WhateverNameYouWant { get; set; }

        public class ValidationHandlerConfigurationSettings
        {

            [JsonProperty("MinimumTrustLevel")]
            public int MinimumTrustLevelFoo { get; set; }

            [JsonProperty("MinimumMatchingTrustLevel")]
            public int MinimumMatchingTrustLevelBar { get; set; }
        }
    }
}

After the calling, I get everything as you desired:

enter image description here


Old Answer:

According to the source code here, it is simply (near) impossible to do what you are requiring. I have tried both JsonProperty and DataContract, none of which are honored by the Binder, simply because the source code itself simply use the property name.

If you still insist, there are 2 possibilities, however I do not recommend any as changing properties' names are much simpler:

  • Fork your source code there, or simply copy that file (in my attempt to trace the code, I rename all methods to something like Bind2, BindInstance2 etc), and rewrite the code accordingly.

  • This one is very specific to current implementation, so it's not future-proof: the current code is calling config.GetSection(property.Name), so you can write your own IConfiguration and provide your own name for GetSection method and tap it into the bootstrap process instead of using the default one.

Upvotes: 1

Abhinaw Kaushik
Abhinaw Kaushik

Reputation: 627

Changing PropertyName on JsonProperty does have effect. Here is the same I tried and it did worked for me:

my JSON data:

{"name": "John","age": 30,"cars": [ "Ford", "BMW", "Fiat" ]}

and the Model:

public class RootObject
    {
        [JsonProperty(PropertyName ="name")]
        public string Apple { get; set; }
        public int age { get; set; }
        public List<string> cars { get; set; }
    }

and here is the code:

RootObject obj = JsonConvert.DeserializeObject<RootObject>(json);

and this is the output i get

enter image description here

You need to set the PropertyName in JsonProperty same as json file property name but your C# model property can be what you wanted, just that they need to be decorated with [JsonProperty(PropertyName ="jsonPropertyName")] Hope this helps you solve your issue.

Happy coding...

Upvotes: -1

Nkosi
Nkosi

Reputation: 247423

That is by design. Binding to POCO via configuration is done by convention. Not like Model Binding to Controller Action parameters.

It matches property names on the POCO to keys in the provided JSON.

Reference Configuration in ASP.NET Core

So either you change the settings to match the class like you showed in the original question, or change the class to match the settings keys in the Json-based configuration file.

[JsonObject(MemberSerialization.OptIn)]
public class RecognitionConfiguration {
    [JsonProperty(PropertyName = "PostProcessing", Required = Required.Always)]
    public PostRecognitionConfiguration PostProcessing{ get; set; }

    [JsonProperty(PropertyName = "Processing", Required = Required.Always)]
    public ProcessRecognitionConfiguration Processing{ get; set; }
}

[JsonObject(MemberSerialization.OptIn)]
public class PostRecognitionConfiguration {
    [JsonProperty(Required = Required.Always)]
    public ValidationHandlerConfiguration ValidationHandlerConfiguration { get; set; }

    [JsonProperty] 
    public List<string> MatchingCharacterRemovals { get; set; }
}

[JsonObject(MemberSerialization.OptIn)]
public class ProcessRecognitionConfiguration {
    [JsonProperty(PropertyName = "OrderSelection", Required = Required.Always)]
    public OrderSelectionConfiguration OrderSelection { get; set; }
}

public partial class ValidationHandlerConfiguration {
    [JsonProperty("MinimumTrustLevel")]
    public long MinimumTrustLevel { get; set; }

    [JsonProperty("MinimumMatchingTrustLevel")]
    public long MinimumMatchingTrustLevel { get; set; }
}


public partial class OrderSelectionConfiguration {
    [JsonProperty("SelectionDaysInterval")]
    public long SelectionDaysInterval { get; set; }

    [JsonProperty("SelectionDaysMaximum")]
    public long SelectionDaysMaximum { get; set; }
}

Upvotes: 4

Related Questions