Eli Weinstock-Herman

Mapping Complex types to/from JSON with JSON.Net

Original post posted on Friday, July 7, 2017 at LessThanDot.com

In an earlier post I introduced a strongly typed Identity object I am using in an ASP.Net Core application to make my code and error messages more readable. I didn’t wanted that extra complexity reflected in my database or over the wire with an API. In this post we’ll look at a simple method to map my strongly typed properties in C# to simpler values in JSON.

This is my desired state:

Transparent Server Side Identity Type

Transparent Server Side Identity Type

I want a strongly typed Identity object in my API backend that transparently converts into a simple int value to/from the database and converts to an int or null for the front-end (null in cases where a permanent ID hasn’t been assigned yet). The right side of this was handled in that earlier post with a PetaPoco IMapper implementation registered globally for IIdentity types. JSON.Net supports a similar method that I can register with ASP.Net Core.

This is what my ASP.Net MVC Method looks like:

C#
1
2
3
4
5
[HttpGet()]
public async Task<List<ApplicationDTO>> GetAllAsync()
{
    return await _databaseStore.Applications.GetAllAsync();
}
[HttpGet()]
public async Task<List<ApplicationDTO>> GetAllAsync()
{
	return await _databaseStore.Applications.GetAllAsync();
}

And this is what we see over the wire:

Javascript
1
2
3
[
  {"id":2, "organizationId":1, "name":"Fictitious Co, LLC Application"}
]
[
  {"id":2, "organizationId":1, "name":"Fictitious Co, LLC Application"}
]

Here is the definition of the ApplicationDTO object:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ApplicationDTO
{   
    [Obsolete("Serialization use only", true)]
    public ApplicationDTO() { }
 
    public ApplicationDTO(AppId id, OrganizationId organizationid, string name)
    {
        Id = id;
        OrganizationId = organizationid;
        Name = name;
    }
    
    public AppId Id { get; set; }
        
    public OrganizationId OrganizationId { get; set; }
        
    public string Name { get; set; }
}
public class ApplicationDTO
{   
    [Obsolete("Serialization use only", true)]
    public ApplicationDTO() { }
 
    public ApplicationDTO(AppId id, OrganizationId organizationid, string name)
    {
        Id = id;
        OrganizationId = organizationid;
        Name = name;
    }
    
    public AppId Id { get; set; }
        
    public OrganizationId OrganizationId { get; set; }
        
    public string Name { get; set; }
}

Here is the definition of the OrganizationId Identity:

C#
1
2
3
4
5
6
7
8
9
10
11
12
public class OrganizationId : IIdentity<int>
{   
    [Obsolete("Serialization use only", true)]
    public OrganizationId() { }
     
    public OrganizationId(int id)
    {
        RawValue = id;
    }
     
    public int RawValue { get; set; }
}
public class OrganizationId : IIdentity<int>
{   
	[Obsolete("Serialization use only", true)]
	public OrganizationId() { }
	 
	public OrganizationId(int id)
	{
		RawValue = id;
	}
	 
	public int RawValue { get; set; }
}

These are both generated code, with some of the extras left out (potentially a future post).

Implementing a JSON.Net Mapper

JSON.Net supports custom JsonConverter implementations that will let us transparently convert between IIdentity objects in C# and int values JSON:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class IdentityJsonConverter<T> : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        writer.WriteValue(((IIdentity<T>)value).RawValue);
    }
 
    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.Value != null)
        {
            var ctor = objectType.GetConstructor(new Type[] { typeof(T) });
            var obj = ctor.Invoke(new object[] { (T)Convert.ChangeType(reader.Value, typeof(T)) });
            return obj;
        }
        else
        {
            return null;
        }
    }
 
    public override bool CanConvert(Type objectType)
    {
        return typeof(IIdentity<T>).IsAssignableFrom(objectType);
    }
}
public class IdentityJsonConverter<T> : JsonConverter
{
    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        writer.WriteValue(((IIdentity<T>)value).RawValue);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        if (reader.Value != null)
        {
            var ctor = objectType.GetConstructor(new Type[] { typeof(T) });
            var obj = ctor.Invoke(new object[] { (T)Convert.ChangeType(reader.Value, typeof(T)) });
            return obj;
        }
        else
        {
            return null;
        }
    }

    public override bool CanConvert(Type objectType)
    {
        return typeof(IIdentity<T>).IsAssignableFrom(objectType);
    }
}

How it works:

CanConvert identifies any implementations of IIdentity as something this converter can handle.

WriteJson is called when JSON.Net is converting to JSON and simply returns the underlying value of my Identity.

ReadJson is called to convert from raw JSON to C#. Converting JSON is slightly more complex, as I allow for null values from the client (it doesn’t generate id’s) so when the JSON value is null I pass that on as a null Identity. If it’s not null, I use reflection on the concrete Identity to find the single parameter constructor and invoke it with incoming JSON converted to the expected type (int for the examples above).

If I wasn’t generating these Identity classes, there would be some risk in assuming the presence of a constructor of that shape. Because I’m generating it, I can save time because i know it’s all or nothing, either all of the Identity objects will work or none of them will.

Employing it in ASP.Net

To use this custom JSONConverter when ASP.Net is serializing/deserializing Action responses and inputs, I add JSON options to MVC during the ConfigureServics call of Startup:

Startup.cs

C#
1
2
3
4
5
6
7
8
9
10
public IServiceProvider ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddMvc()
            .AddJsonOptions(options => {
                options.SerializerSettings.Converters.Add(new IdentityJsonConverter<Int32>());
            });
 
   // ...
}
public IServiceProvider ConfigureServices(IServiceCollection services)
{
    // Add framework services.
    services.AddMvc()
            .AddJsonOptions(options => {
                options.SerializerSettings.Converters.Add(new IdentityJsonConverter<Int32>());
            });

   // ...
}

Now all attempts to deserialize a value into an IIdentity property and serialization to respond with one of these values will pass through the custom mapper and I have the benefit of my custom type in my server-side logic without any extra overhead in my client-side app or code to write as I add new models or properties.

Comments are available on the original post at lessthandot.com