Reputation: 5101
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
Reputation: 401
You have two solutions:
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:
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
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