Rodrigo Martins
Rodrigo Martins

Reputation: 43

Custom JsonConverter on a property based on another property value

I have a physical dimension class like this:

public class Physical
{
  public Dimension Dimension {get; set;}
  public double Value {get; set;}
  public string Unit {get; set;}
}

Dimension is an enum with values like, Force, Temperature, Displacement, Time, etc.

and a class with Physical properties like

public class MeasurementInfo
{
  public Instrument Instrument {get; set;}
  public Physical MaxReading {get; set;}
  public Physical MinReading {get; set;}
  public Physical AmbientTemperature {get; set;}
}

Instrument is also an enum with values like Chronometer, WeightScale, Thermometer, Caliper, etc.

The Dimension property value of some of my Physical properties depends on the Instrument value. Other ones are fixed. Example:

var myMeasure = new MeasurementInfo()
{
  Instrument = Instrument.WeightScale,
  MaxReading = new Physical()
  {
    Dimension = Dimension.Weight,
    Value = 100.0,
    Unit = "kg"
  },
  MinReading = new Physical()
  {
    Dimension = Dimension.Weight,
    Value = 50.0,
    Unit = "kg"
  },
  AmbientTemperature = new Physical()
  {
    Dimension = Dimension.Temperature,
    Value = 27,
    Unit = "°C"
  }
};

What I want is to save this object as JSON like this:

{
  "Instrument": "Weight Scale",
  "Max Reading": "100 kg",
  "Min Reading": "50 kg",
  "AmbientTemperature": "27 °C"
}

Serialization is easy as we have Value and Unit defined. My problem is with deserialization because I have to read the Instrument value to determine a Dimension to recreate the Physical objects.

My actual try is using a ContractResolver. This way I can define my JsonConverter based on the property type and name.

public class MyContractResolver : DefaultContractResolver
{
  protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
  {
    JsonProperty result = base.CreateProperty(member, memberSerialization);
    if(result.PropertyType == typeof(Physical))
    {
      var property = member as PropertyInfo;
      switch (result.PropertyName)
      {
        case "AmbientTemperature":
          result.Converter = new JsonPhysicalConverter(Dimension.Temperature);
          break;
        case "MaxReading":
        case "MinReading":
          result.Converter = new JsonPhysicalConverter(???);
          break;
      }
    }
  }
}

The ??? is where I got stuck. The ContractResolver doesn't work for instances so I can't know beforehand my Instrument value.

Upvotes: 3

Views: 5494

Answers (2)

Brian Rogers
Brian Rogers

Reputation: 129667

A JsonConverter does not have access to the parent of the object that it is handling. So if the converter handles Physical it won't be able to "see" the Instrument inside MeasurementInfo.

So there are two approaches I can see:

  1. Make the converter handle the parent MeasurementInfo in addition to Physical. The converter will then be able to see the Instrument and create Physical as needed.
  2. Use the unit value to determine the appropriate Dimension. For example, if the unit is kg you know the dimension must be Weight, regardless of the instrument, right? This would only break down if there are two units which look the same but represent different dimensions depending on the instrument. But based on your examples, I don't think this would be the case. This approach would be "cleaner" in my opinion.

Here is an example of the first approach:

public class MeasurementInfoConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(MeasurementInfo);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JObject obj = JObject.Load(reader);
        MeasurementInfo info = new MeasurementInfo();
        info.Instrument = obj["Instrument"].ToObject<Instrument>(serializer);
        info.MinReading = ReadPhysical(obj, "Min Reading", info.Instrument);
        info.MaxReading = ReadPhysical(obj, "Max Reading", info.Instrument);
        info.AmbientTemperature = ReadPhysical(obj, "Ambient Temperature", Instrument.Thermometer);
        return info;
    }

    private Physical ReadPhysical(JObject obj, string name, Instrument instrument)
    {
        Dimension dim = Dimension.Force;
        switch (instrument)
        {
            case Instrument.WeightScale: dim = Dimension.Weight; break;
            case Instrument.Chronometer: dim = Dimension.Time; break;
            case Instrument.Thermometer: dim = Dimension.Temperature; break;
            case Instrument.Caliper:     dim = Dimension.Displacement; break;
        }
        string[] parts = ((string)obj[name]).Split(new char[] { ' ' }, 2);
        Physical physical = new Physical()
        {
            Dimension = dim,
            Value = double.Parse(parts[0]),
            Unit = parts[1]
        };
        return physical;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        MeasurementInfo info = (MeasurementInfo)value;
        JObject obj = new JObject();
        obj.Add("Instrument", JToken.FromObject(info.Instrument, serializer));
        WritePhysical(obj, "Min Reading", info.MinReading);
        WritePhysical(obj, "Max Reading", info.MaxReading);
        WritePhysical(obj, "Ambient Temperature", info.AmbientTemperature);
        obj.WriteTo(writer);
    }

    private void WritePhysical(JObject obj, string name, Physical physical)
    {
        obj.Add(name, physical.Value.ToString("N0") + " " + physical.Unit);
    }
}

Working round-trip demo: https://dotnetfiddle.net/ZUibQ1


For completeness, here is an example of the second approach:

public class PhysicalConverter : JsonConverter
{
    public override bool CanConvert(Type objectType)
    {
        return objectType == typeof(Physical);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        string value = (string)reader.Value;
        string[] parts = value.Split(new char[] { ' ' }, 2);
        Dimension dim;
        if (!DimensionsByUnit.TryGetValue(parts[1], out dim)) dim = Dimension.Force;
        Physical physical = new Physical()
        {
            Dimension = dim,
            Value = double.Parse(parts[0]),
            Unit = parts[1]
        };
        return physical;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        Physical physical = (Physical)value;
        writer.WriteValue(physical.Value.ToString("N0") + " " + physical.Unit);
    }

    private static Dictionary<string, Dimension> DimensionsByUnit = new Dictionary<string, Dimension>
    {
        { "mg", Dimension.Weight },
        { "g", Dimension.Weight },
        { "kg", Dimension.Weight },
        { "°C", Dimension.Temperature },
        { "°F", Dimension.Temperature },
        { "°K", Dimension.Temperature },
        { "µs", Dimension.Time },
        { "ms", Dimension.Time },
        { "s", Dimension.Time },
        { "mm", Dimension.Displacement },
        { "cm", Dimension.Displacement },
        { "m", Dimension.Displacement },
    };
}

Working round-trip demo: https://dotnetfiddle.net/1ecLNJ

Upvotes: 4

Prachi
Prachi

Reputation: 3574

Use anonymous object:

        var objectToBeSerialized = new
        {
            Instrument = myMeasure.Instrument.ToString(),
            MaxReading = $"{myMeasure.MaxReading.Value} {myMeasure.MaxReading.Unit}",
            MinReading = $"{myMeasure.MinReading.Value} {myMeasure.MinReading.Unit}"
        };

and convert this object into JSON using NewtonSoft.Json library:

        var serializedJSON = JsonConvert.SerializeObject(objectToBeSerialized);

Upvotes: 0

Related Questions