Search code examples
c#json.netjson-deserialization

Deserializing json - mapping single property with no direct json match


I am trying to deserialize an existing JSON structure to into an object composed of a set of models. The naming in these models are not consistent and I was specifically asked to not change them (renaming, adding attributes, etc).

So, given this Json text (just a small sample):

{
  "parameter": {
      "alarms": [
      {
          "id": 1,
          "name": "alarm1",
          "type": 5,
          "min": 0,
          "max": 2
      }],
      "setting-active": true,
      "setting-oneRun": true
   }
}

would need to be mapped into these models:

public class Alarm
{
    public int AlarmId { get; set; }
    public string AlarmName { get; set; }
    public AlarmType RbcType { get; set; }
    public int MinimumTolerated { get; set; }
    public int MaximumTolerated { get; set; }
}

public class Setting
{
    public bool Active { get; set; }
    public bool OneRun { get; set; }
}

public class Parameter
{
    public List<Alarm> Alarms { get; set; }
    public Setting ParameterSetting { get; set; }
}

So far, im writing a class that extends DefaultContractResolver and overrides maps property names.

MyCustomResolver so far:

public class MyCustomResolver : DefaultContractResolver
{
   private Dictionary<string, string>? _propertyMappings;

   protected override IList<JsonProperty> CreateProperties(Type type, MemberSerialization memberSerialization)
   {
       //ModelMappings is a static class that will return a dictionary with mappings per ObjType being deserialized
       _propertyMappings = ModelMappings.GetMapping(type);
       return base.CreateProperties(type, memberSerialization);
   }

   protected override string ResolvePropertyName(string propertyName)
   {
       if (_propertyMappings != null)
       {
           _propertyMappings.TryGetValue(propertyName, out string? resolvedName);
           return resolvedName ?? base.ResolvePropertyName(propertyName);
       }
       return base.ResolvePropertyName(propertyName);
   }

}

Code that Im using to deserialize:

var settings = new JsonSerializerSettings();
settings.DateFormatString = "YYYY-MM-DD";
settings.ContractResolver = new MyCustomResolver();
Parameter p = JsonConvert.DeserializeObject<Parameter>(jsonString, settings);

So I reached a point I need to somehow map the properties in Parameter to values located in the prev json node ("setting-active", "setting-oneRun"). I need to tell the deserializer where these values are. Can this be done using an extension of DefaultContractResolver ?

I appreciate any tips pointing in the right direction


Solution

  • You can apply ModelMappings.GetMapping(objectType) in DefaultContractResolver.CreateObjectContract():

    public class MyCustomResolver : DefaultContractResolver
    {
        protected override JsonObjectContract CreateObjectContract(Type objectType)
        {
            var contract = base.CreateObjectContract(objectType);
            var overrides = ModelMappings.GetMapping(objectType);
            if (overrides != null)
            {
                foreach (var property in contract.Properties.Concat(contract.CreatorParameters))
                {
                    if (property.UnderlyingName != null && overrides.TryGetValue(property.UnderlyingName, out var name))
                        property.PropertyName = name;
                }
            }
            return contract;
        }
    }
    

    Notes:

    • By applying the mappings in CreateObjectContract() you can remap both property names and creator parameter names.

    • Since the contract resolver is designed to resolve contracts for all types, storing a single private Dictionary<string, string>? _propertyMappings; doesn't really make sense.

    • Unlike your previous question, your current question shows properties from a nested c# object ParameterSetting getting percolated up to the parent object Parameter. Since a custom contract resolver is designed to generate the contract for a single type, it isn't suited to restructuring data between types. Instead, consider using a DTO or converter + DTO in such situations:

      public class ParameterConverter : JsonConverter<Parameter>
      {
          record ParameterDTO(List<Alarm> alarms, [property: JsonProperty("setting-active")] bool? Active, [property: JsonProperty("setting-oneRun")] bool? OneRun); 
      
          public override void WriteJson(JsonWriter writer, Parameter? value, JsonSerializer serializer)
          {
              var dto = new ParameterDTO(value!.Alarms, value.ParameterSetting?.Active, value.ParameterSetting?.OneRun);
              serializer.Serialize(writer, dto);
          }
      
          public override Parameter? ReadJson(JsonReader reader, Type objectType, Parameter? existingValue, bool hasExistingValue, JsonSerializer serializer)
          {
              var dto = serializer.Deserialize<ParameterDTO>(reader);
              if (dto == null)
                  return null;
              existingValue ??= new ();
              existingValue.Alarms = dto.alarms;
              if (dto.Active != null || dto.OneRun != null)
                  existingValue.ParameterSetting = new () { Active = dto.Active.GetValueOrDefault(), OneRun = dto.OneRun.GetValueOrDefault() };
              return existingValue;
          }
      }
      

      If your "real" model is too complex to define a DTO, you could create a JsonConverter<Paramater> that (de)serializes the JSON into an intermediate JToken hierarchy, then restructures that. See e.g. this answer to Can I serialize nested properties to my class in one operation with Json.net?.

    • In some cases, the custom naming of your properties is just camel casing. To camel case property names without the need for explicit overrides, set MyCustomResolver.NamingStrategy to CamelCaseNamingStrategy e.g. as follows:

      var settings = new JsonSerializerSettings
      {
          DateFormatString = "YYYY-MM-DD",
          // Use CamelCaseNamingStrategy since many properties in the JSON are just camel-cased.
          ContractResolver = new MyCustomResolver { NamingStrategy = new CamelCaseNamingStrategy() },
          Converters = { new ParameterConverter() },
      };
      

    Demo fiddle here.