Search code examples
c#jsonjson.net

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?


Solution

  • 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