1

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

Jaime Oliveira
  • 751
  • 1
  • 5
  • 13
  • Please provide a complete [mre]. – Julia Dec 29 '22 at 12:25
  • @Julia - ok it's done – Jaime Oliveira Dec 29 '22 at 12:45
  • It sounds like you're having to fix a problem caused by bad system design. Probably some javascript developer hacked together some code and now they decided it also has to be handled by a strongly typed language. Go to the system architect and please tell him he f#cked up IMHO. You should start with the strong types. So a refractor seems in place. – JHBonarius Dec 29 '22 at 13:12
  • [this answer](https://stackoverflow.com/a/38112291/3744182) suggests to use a `private Dictionary> memberNameToJsonNameMap;` not a `Dictionary`. The contract resolver is used to resolve mappings for all types, so overrides in general will need to be on a per-type basis. – dbc Dec 29 '22 at 15:42
  • Anyway, I'm not sure I understand your question. Is it, *how can I apply my `_propertyMappings` to the properties returned by `base.CreateProperties(type, memberSerialization)`?* – dbc Dec 29 '22 at 15:44
  • Your required JSON is malformed. If I upload it to https://jsonlint.com/ I get an error *`Error: Parse error on line 12: Expecting '}', ',', got 'EOF'`*. Specifically, your JSON is missing a closing brace. Can you please [edit] your question to clarify your required JSON please? – dbc Dec 29 '22 at 16:03
  • More importantly, in your data model the settings are shown to be in a nested object `Setting` but in your JSON it's not clear they are nested thanks to the missing brace(s). In your [previous questions](https://stackoverflow.com/q/74930529/3744182) `"settings"` clearly mapped to an array of objects, in the current question it is no longer clear that the structure of your JSON maps to the structure of your model. Please [edit] your question to clarify. – dbc Dec 29 '22 at 16:07
  • @dbc I fixed the JSON. Sorry for the confusion. ModelMappings.GetMapping(type) will return a simple dictionary for the type being handled, its a slight deviation from having a type based dictionary but it's the same idea. This dictionary will be used in the ResolvePropertyName method. – Jaime Oliveira Dec 29 '22 at 16:18
  • I am asking a question for this specific problem, and this Json is not the real imense json and models I am dealing with, and this question is in no way related to previous questions. Just a simple example the issue Im trying to sort out atm. Thanks for pointing out the json was incorrect. – Jaime Oliveira Dec 29 '22 at 16:23

2 Answers2

1

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.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • Thanks for the comprehensive response on this one. It makes perfect sense. The models Im working with are big, but the DTO approach is reasonable and simple. Im now using the naming strategy and leaving the dictionaries only for the cases where the property names are not matching. Thanks for your time :) – Jaime Oliveira Dec 30 '22 at 09:48
0

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".

Fabien Sartori
  • 235
  • 3
  • 10
  • The contract resolver is in fact doing the mapping between the json and the business object, so apart from separating tasks to individual libraries, this doesn't add much value. – JHBonarius Dec 29 '22 at 13:20