Jean
Jean

Reputation: 5101

Deserialize to existing instance of classes in C#

This can be applied to several problems where there is a list with a limited number of items like the list of cities, currencies, languages etc.

I am trying to find a way to serialize a class to its Id and then deserialize back to its complete description. For example, if we have a city structure like this:

public struct City
{
    public string Name;
    public Country Country;
}

[JsonObject(MemberSerialization.OptIn)]
public class Country
{
    public Country(string code, string name) { Code = code; Name = name; }
    [JsonProperty]
    public string Code;
    public string Name;
}

public class Countries
{
    public static List<Country> All = new List<Country>()
    {
        new Country("US", "United States"),
        new Country("GB", "United Kingdom"),
        new Country("FR", "France"),
        new Country("ES", "Spain"),
    };
}

I don't mind the Country serialized as {"code":"ES"} or simply "ES", but I would like it to be deserialized as the existing instance of country in Countries.All

How could I get this behavior?

Upvotes: 1

Views: 1481

Answers (2)

Guillaume Mercier
Guillaume Mercier

Reputation: 401

You have two solutions:

  • Use enums and extension functions, which would simplify the verification process as well enums are limited specifically values
  • Use custom JsonConverters to convert your JSON data using ids/code, which would allow you to customize how they are serialized into JSON

I'll have examples up soon, I need to type em out before through. Edit: Examples completed


In both cases
In both examples, I used a JsonConvertAttribute to set which converter should be used when serializing or deserializing an object. This parameter tells the json.net library which class/converter to use when serializing the object/parameter by default.

If you only need the serialization at specific moments you have to options for 2 different scenarios:

  • When serializing an array/list
    • Add a JsonPropertyAttribute to the property and set the ItemConverterType to the type of your converter.
  • When serializing anything else
    • Add a JsonConvertAttribute to the property and in the constructor, pass the JSON converter.

Keeping country to an object/struct
This option is in my opinion the most flexible and practical of the 2 solutions as it allows for changing requirements and less exception throwing.

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyApplication
{

    public struct City
    {
        public string Name;
        // you could also set a converter for this field specifically if you only need for specific fields but also
        // still want it to display as a normal json object when you serialise the object.
        // [JsonConverter(typeof(CountryConverter))]
        public Country Country;
    }

    // Setting a json converter attribute allows json.net to understand that an object by default
    // will be serialised and deserialised using the specified converter.
    [JsonConverter(typeof(CountryConverter))]
    public class Country
    {
        public Country(string code)
        {
            switch (code)
            {
                case "US": Name = "United-States"; break;
                case "GB": Name = "United Kingdom"; break;
                case "FR": Name = "France"; break;
                case "ES": Name = "Spain"; break;
                case "CA": Name = "Canada"; break;
            }
        }
        public string Code { get; set; }
        public string Name { get; set; }
    }


    public class CountryConverter : JsonConverter<Country>
    {
        // Assuming that the countries are serialised using the code
        public override Country ReadJson(JsonReader reader, Type objectType, Country existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            if(reader.Value == null && reader.ValueType == typeof(string))
            {
                return null;
            }

            string code = (string) reader.Value;
            code = code.ToUpperInvariant(); // Because reducing error points is usually a good thing

            return new Country(code);

        }

        public override void WriteJson(JsonWriter writer, Country value, JsonSerializer serializer)
        {
            //Writes the code as the value for the object
            writer.WriteValue(value.Code);
        }
    }
}

Changing country into an enum
Using an enum can be great as it allows you to have a set of unchanging values that don't have to create new objects over and over. But it makes your conversion logic a tad bit more complicated.

using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace MyApplication
{

    public struct City
    {
        public string Name;
        public Country Country;
    }

    // Using the full name as it makes it easier to work with and also because you'd need a json converter
    // if you want a string and not the number/index of the country when you serialise the data
    [JsonConverter(typeof(CountryConverter))]
    public enum Country
    {
        UnitedStates,
        UnitedKingdom,
        France,
        Spain,
        Canada
    }

    // this class only exists for you to add extentions to this enums.
    // In short extentions are a type of methods added to other classes that act
    // as if they were part of outher classes. usually this means that the first parameter
    // is prefixed by this.
    public static class CountryExtentions
    {
        public static string GetCode(this Country country)
        {
            switch (country)
            {
                case Country.UnitedStates: return "US";
                case Country.UnitedKingdom: return "GB";
                case Country.France: return "FR";
                case Country.Spain: return "SP";
                case Country.Canada: return "CA";
                default: throw new InvalidOperationException($"This country has no code {country.ToString()}");
            }
        }
    }
    public class CountryConverter : JsonConverter<Country>
    {
        // Assuming that the countries are serialised using the code
        public override Country ReadJson(JsonReader reader, Type objectType, Country existingValue, bool hasExistingValue, JsonSerializer serializer)
        {
            // make sure you can convert the thing into a string
            if (reader.Value == null && reader.ValueType == typeof(string))
            {
                throw new InvalidOperationException($"The data type passed {reader.ValueType.Name} isn't convertible. The data type musts be a string.");
            }

            // get the value
            string code = (string)reader.Value;
            code = code.ToUpperInvariant(); // Because reducing error points is usually a good thing

            // cycle through the enum values to compare them to the code
            foreach (Country country in Enum.GetValues(typeof(Country)))
            {
                // if the code matches
                if (country.GetCode() == code)
                {
                    // return the country enum
                    return country;
                }
            }
            // if no match is found, the code is invlalid
            throw new InvalidCastException("The provided code could not be converted.");

        }

        public override void WriteJson(JsonWriter writer, Country value, JsonSerializer serializer)
        {
            //Writes the code as the value for the object
            writer.WriteValue(value.GetCode());
        }
    }
}

Upvotes: 2

ProgrammingLlama
ProgrammingLlama

Reputation: 38785

I would recommend using a JsonConverter like so:

public class CountryConverter : JsonConverter
{
    public override bool CanRead { get { return true; } }
    public override bool CanWrite { get { return false; } }

    public override bool CanConvert(Type objectType)
    {
        return typeof(Country) == objectType;
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var obj = JObject.Load(reader);
        var code = obj.GetValue("code", StringComparison.OrdinalIgnoreCase)?.Value<string>();
        if (code != null)
        {
            return Countries.All.FirstOrDefault(c => string.Equals(c.Code, code, StringComparison.OrdinalIgnoreCase));
        }
        return null;
    }

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

It doesn't take the responsibility of serializing objects, only deserializing them. To deserialize them, it reads the "code" field, and then returns the first matching country from the Countries.All list. It would probably be best (more efficient) to make this use a dictionary instead.

To use this, simply decorate your country class like so:

[JsonConverter(typeof(CountryConverter))]
public class Country

Upvotes: 1

Related Questions