Gianpiero
Gianpiero

Reputation: 3547

Custom deserializer for a field with multiple value types using Json.NET

I have a JSON file where the temperature is sometimes a number (ver. 1) and sometimes an object (ver. 2):

// ver. 1
{ 
    "lat": 44.47,
    "lon": 11.43,
    "timezone": "Europe/Rome",
    "temperature": 4.14,
}

// ver. 2
{ 
    "lat": 44.47,
    "lon": 11.43,
    "timezone": "Europe/Rome",
    "temperature": {
        "min": 1.1,
        "max": 12.3
    }
}

To represent these two 'almost equal' formats I'd like to have only one class with (possibly?) a custom converter for the temperature. All the other fields will be converted automatically:

public class Forecast {
    private Coordinates _coord = new Coordinates();
    private Temperature _temperature = new Temperature();

    // json.net will do everything to convert 'lat' in a double 
    
    [JsonProperty("lat")]
    public double Latitude { 
        get => _coord.Latitude; 
        set => _coord.Latitude = value;
    }

    // json.net will do everything to convert 'lon' in a double 
    [JsonProperty("lon")]
    public double Longitude {
        get => _coord.Longitude; 
        set => _coord.Longitude = value;
    }

    // json.net will do everything to convert 'timezone' in a string 
    [JsonProperty("timezone")]
    public string TimeZone{ get; set; }

    // HERE I WANT TO USE A CUSTOM CONVERTER THAT HANDLES the 2 VERSIONS of 'temp' IN THE PROPER WAY.
    // THE CODE BELOW DOES NOT WORK AT ALL!
    [JsonProperty("temperature", ItemConverterType = typeof(MyTemperatureConverter))]
    public Object Temp {
        get => _temperature;    // return a Temperature object
        set {
            // do something here to handle it? E.g.:
            if (value is Double) { 
                // it is a ver. 1
                _temperature.Avg = (value as double);
            } else {
                // it is a ver. 2
               _temperature.Max = (value as Temperature).Max;
               _temperature.Min = (value as Temperature).Min;
            }
        }    
    }
}

public class Coordinates {
    public double Latitude { get; set; }
    public double Longitude { get; set; }
}

public class Temperature {
    public double? Avg { get; set; }
    public double? Min { get; set; }
    public double? Max { get; set; }
}

Here is my (hypothetical) deserializer:

public class MyTemperatureConverter : JsonConverter {

    public override void WriteJson(JsonWriter writer, [AllowNull] object value, JsonSerializer serializer) {
        throw new NotImplementedException();
    }

    public override object ReadJson(JsonReader reader, Type objectType, Object existingValue, JsonSerializer serializer) {
        // WHAT TO DO HERE?
    }

}

And here is my general code to read the JSON:

protected virtual async Task<Forecast> ParseForecastFromResponse(HttpResponseMessage response) {
    using (var responseStream = await response.Content.ReadAsStreamAsync().ConfigureAwait(false)) {
        using (var jsonReader = new Newtonsoft.Json.JsonTextReader(new System.IO.StreamReader(responseStream))) {
            var serializer = new Newtonsoft.Json.JsonSerializer();
            return serializer.Deserialize<Forecast>(jsonReader);
        }
    }
}

The question is: am I on the right track? Or do I need to go in a different direction?

Upvotes: 1

Views: 149

Answers (1)

Brian Rogers
Brian Rogers

Reputation: 129777

You can make a converter to handle both formats like this:

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

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        Temperature temp = new Temperature();
        JToken token = JToken.Load(reader);
        if (token.Type == JTokenType.Object)
        {
            temp.Min = (double?)token["min"];
            temp.Max = (double?)token["max"];
        }
        else
        {
            temp.Avg = (double?)token;
        }
        return temp;
    }

    public override bool CanWrite => false;

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        throw new NotImplementedException();
    }
}

In your Forecast class, the Temperature property should be defined and annotated like this:

    [JsonProperty("temperature"), JsonConverter(typeof(MyTemperatureConverter))]
    public Temperature Temperature { get; set; } = new Temperature();

Here is a working demo: https://dotnetfiddle.net/3yrUU3

Upvotes: 2

Related Questions