Home > other >  Deserializing json - mapping single property with no direct json match
Deserializing json - mapping single property with no direct json match

Time:12-30

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

CodePudding user response:

I think that the best way to "KEEP IT SIMPLE", you need to define an object that has exactly the properties of the json. Then you can use a library like "Automapper" to define rules of mapping between the "json object" and the "business object".

CodePudding user response:

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.

  • Related