Quantcast
Channel: Andrew Lock | .NET Escapades
Viewing all articles
Browse latest Browse all 743

Serializing a PascalCase Newtonsoft.Json JObject to camelCase

$
0
0

In this post I describe one of the quirks of serializing JSON.NET JObject (contract resolvers are not honoured), and show how to get camelCase names when serializing a JObject that stored its property names in PascalCase.

Background - using JObject for dynamic data

I was working with some code the other day that stored objects in PostgreSQL using the built-in JSON support. The code that used it was deserializing the data to a JSON.NET JObject in code. So the data class looked something like the following, with a JObject property, Details:

public class Animal
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public string Genus { get; set; }
    public JObject Details { get; set; }
}

In this case, the JObject was a "serialized" version of a data class:

public class SlothDetails 
{
    public int NumberOfToes { get; set; }
    public int NumberOfCervicalVertebrae { get; set; }
}

So an instance of the Animal class was created using code similar to the following:

var sloth = new Animal
{
    Id = Guid.NewGuid(),
    Name = "Three-toed sloth",
    Genus = "Bradypus",
    Details = JObject.FromObject(new SlothDetails
    {
        NumberOfToes = 3,
        NumberOfCervicalVertebrae = 9,
    })
};

In this code we take the strongly-typed SlothDetails and turn it into a JObject. There's nothing very special here - we're using JObject as a pseudo-dynamic type, to allow us to store different types of data in the Details property. For example, we could create an entirely different type and assign it to the Details property:

public class DogDetails 
{
    public bool IsGoodBoy { get; set; }
    public int NumberOfLegs { get; set; }
}

var dog = new Animal 
{
    Id = Guid.NewGuid(),
    Name = "Dog",
    Genus = "Canis",
    Details = JObject.FromObject(new DogDetails
    {
        IsGoodBoy = true,
        NumberOfLegs = 4,
    })
};

We can work with both Animal instances in the same way, even though the Details property contains different data.

So why would you want to do this? The type system is one of the strong points of C#, and if you need truly dynamic data, there's always the dynamic type from C# 4.0? Why not create Animal<T> and make Details a T? Or we could have just made Details a string, and stored a serialized version of the data?

All of those might be valid approaches for your situation. In our case, we know that we're storing JSON in the database, and that the Details object must serialize to JSON, so it made sense to use a type that most accurately represents that data: JObject. LINQ to JSON provides a convenient API to query the data, and we get some type safety from knowing that anything passed to Details is a valid JSON object.

All of this works perfectly, until you try exposing one of these JObject from an ASP.NET Web API.

JSON.NET, serialization, and camelCase

Lets start by seeing what you get if you Serialize the dog instance above, by returning it from an ASP.NET Core controller:

[ApiController, Route("api/[controller]")]
public class AnimalsController
{
    [HttpGet]
    public object Get()
    {
        return new Animal
        {
            Id = Guid.NewGuid(),
            Name = "Dog",
            Genus = "Canis",
            Details = JObject.FromObject(new DogDetails
            {
                IsGoodBoy = true,
                NumberOfLegs = 4,
            })
        };
    }
}

ASP.NET Core uses a camelCase formatter by default (instead of the PascalCase used for C# property names) so the resulting JSON is camelCase:

{
    "id": "96ca7c68-7550-4809-86c5-4d784f3b3f87",
    "name": "Dog",
    "genus": "Canis",
    "details": {
        "IsGoodBoy": true,
        "NumberOfLegs": 4
    }
}

This looks nearly right, but there's a problem - the IsGoodBoy and NumberOfLegs properties of the serialized JObject are all PascalCase - that's a problem!

This all comes down to an early design-decision (since lamented) that means that contract resolvers are ignored by default when serializing JObject as part of an object graph (as we have here).

This is clearly an issue if you're using a JObject in your object graph. I only found a few ways to work around the limitation, depending on your situation.

1. Change the global serialization settings

The first option is to change the global serialization options to use camelCase. This option has been available for a long time (since version 5.0 of JSON.NET), and will globally configure the JSON.NET serializer to use camelCase, even for a JObject created using PasalCase property names:

// Add this somewhere in your project, most likely in Startup.cs
JsonConvert.DefaultSettings = () => new JsonSerializerSettings
{
    ContractResolver = new CamelCasePropertyNamesContractResolver()
};

If you add this code to your project, your JObject will be serialized to camelCase, with no other changes required to your project:

{
    "id": "96ca7c68-7550-4809-86c5-4d784f3b3f87",
    "name": "Dog",
    "genus": "Canis",
    "details": {
        "isGoodBoy": true,
        "numberOfLegs": 4
    }
}

The obvious downside to this approach is that affects all serialization in your app. If you have a new (or small app) that might not be a problem, but for a large, legacy app that might cause issues. Subtle changes in casing in client-side JavaScript apps could cause a multitude of bugs if you have code relying on the existing JObject serialization behaviour.

2. Don't create PascalCase JObjects

The serialization problem we're seeing stems from two things:

  1. JObject serialization doesn't honour contract resolvers (unless you set JsonConvert.DefaultSettings)
  2. We have a PascalCase JObject instead of camelCase.

The first approach (changing the default serializer) addresses point 1., but the other option is to never get ourselves into this situation! This approach is easier to introduce to large apps, as it allows you to change the "stored format" in a single location, instead of affecting a whole large app.

The JObject.FromObject() method takes a second parameter, which allows you to control how the JObject is created from the C# object. We can use that to ensure the JObject we create uses camelCase names already, so we don't have to worry when it comes to serialization.

To do this, create a JsonSerializer with the required settings. You can store this globally and reuse it throughout your app:

static JsonSerializer _camelCaseSerializer = JsonSerializer.Create(
    new JsonSerializerSettings
    {
        ContractResolver = new CamelCasePropertyNamesContractResolver()
    });

Now, when creating the JObject from a .NET object, pass in the _camelCaseSerializer:

var details = JObject.FromObject(new DogDetails
{
    IsGoodBoy = true,
    NumberOfLegs = 4,
}, _camelCaseSerializer) // <- add second parameter

This will use camelCase for the internal names, so when the JObject is serialized, it will give the output we want:

{
    "isGoodBoy": true,
    "numberOfLegs": 4
}

This approach is probably the best in general - it addresses the problem at its source, and doesn't have to impact your app globally. Unfortunately, if you're already storing objects using PascalCase, this approach might not be feasible to adopt. In which case you're left with the following approach.

3. Convert a PascalCase JObject to camelCase

In some cases, you might just be stuck with a PascalCase JObject as part of an object graph, that needs to be serialized using camelCase. The only solution I could find to this is to create a new camelCase JObject from the existing PascalCase JObject.

The following extension method (and helpers) show how to create a new JObject that has camelCase property names, from one that has PascalCase names.

using Newtonsoft.Json.Linq;
using System.Diagnostics.Contracts;
using System.Linq;
public static class JTokenExtensions
{
    // Recursively converts a JObject with PascalCase names to camelCase
    [Pure]
    static JObject ToCamelCase(this JObject original)
    {
        var newObj = new JObject();
        foreach (var property in original.Properties())
        {
            var newPropertyName = property.Name.ToCamelCaseString();
            newObj[newPropertyName] = property.Value.ToCamelCaseJToken();
        }

        return newObj;
    }

    // Recursively converts a JToken with PascalCase names to camelCase
    [Pure]
    static JToken ToCamelCaseJToken(this JToken original)
    {
        switch (original.Type)
        {
            case JTokenType.Object:
                return ((JObject)original).ToCamelCase();
            case JTokenType.Array:
                return new JArray(((JArray)original).Select(x => x.ToCamelCaseJToken()));
            default:
                return original.DeepClone();
        }
    }

    // Convert a string to camelCase
    [Pure]
    static string ToCamelCaseString(this string str)
    {
        if (!string.IsNullOrEmpty(str))
        {
            return char.ToLowerInvariant(str[0]) + str.Substring(1);
        }

        return str;
    }
}

The ToCamelCase() method starts by creating a new (empty) JObject instance. It then loops through the original object, converting each property name to camelCase. It then recursively converts the Value of the property to a camelCase JToken. This is important, as the properties could also contain JObjects, or JArrays of JObjects, and we need to make sure all the properties are converted to camelCase, not just the "top-level" properties.

For objects other than JObject and JArray, (e.g. number, string), I create a copy of the JToken using DeepClone(). I don't believe that's strictly necessary, but decided to play it safe.

You can use the ToCamelCase() extension method at the "edges" of your system, when you need to serialize a JObject that is stored in PascalCase. This creates a clone of the object, but using camelCase properties.

[ApiController, Route("api/[controller]")]
public class AnimalsController
{
    [HttpGet]
    public object Get()
    {
        var details = JObject.FromObject(new DogDetails
        {
            IsGoodBoy = true,
            NumberOfLegs = 4,
        });

        // Create a clone of the object but with camelCase property names
        return details.ToCamelCase();
    }
}

This approach feels pretty hacky, but it gets the job done when the other approaches are no good for you. For completeness, the following set of xUnit Theory tests show the behaviour in action:

using FluentAssertions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Linq;
using Xunit;

public class JTokenExtensionTests
{
    [Theory]
    [MemberData(nameof(GetTokenData))]
    public void ToCamelCase_WhenNotJObject_DoesNotChangeOutput(JToken original)
    {
        var newToken = original.ToCamelCase();

        var originalSerialized = JsonConvert.SerializeObject(original);
        var newSerialized = JsonConvert.SerializeObject(newToken);

        newSerialized.Should().Be(originalSerialized);
    }

    [Theory]
    [MemberData(nameof(GetJobjectData))]
    public void ToCamelCase_WhenJObject_ConvertsToCamelCase(JObject original, JObject expected)
    {
        var newObject = original.ToCamelCase();

        var expectedSerialized = JsonConvert.SerializeObject(expected);
        var newSerialized = JsonConvert.SerializeObject(newObject);

        newSerialized.Should().Be(expectedSerialized);
    }

    public static IEnumerable<object[]> GetTokenData()
    {
        return GetTokens().Select(token => new object[] { JToken.FromObject(token) });
    }

    public static IEnumerable<object[]> GetJobjectData()
    {
        return GetJObjects().Select(pair => new object[] { JObject.FromObject(pair.Original), JObject.FromObject(pair.Expected) });
    }

    static IEnumerable<(object Original, object Expected)> GetJObjects()
    {
        yield return (
            new { MyVal = 23 },
            new { myVal = 23 });
        yield return (
            new { MyVal = true },
            new { myVal = true });
        yield return (
            new { MyVal = "this is my string" },
            new { myVal = "this is my string" });
        yield return (
            new { MyVal = new[] { 0, 2, 3 } },
            new { myVal = new[] { 0, 2, 3 } });
        yield return (
            new { MyVal = new { A = new { NESTED = new object[] { new { Another = 123 }, new { EEK = false }, } } } },
            new { myVal = new { a = new { nESTED = new object[] { new { another = 123 }, new { eEK = false }, } } } });
    }

    static IEnumerable<object> GetTokens()
    {
        yield return 0;
        yield return true;
        yield return "this is my string";
        yield return "This is my string";
        yield return new[] { 0, 2, 3 };
        yield return DateTime.Now;
        yield return 23.5;
    }
}

Summary

In this post I described some of the quirks of using JObject, in particular the way it doesn't honour the contract resolver settings used to serialize a given object graph. I described three different ways to work around the behaviour: set the global serializations settings, store the JObject using camelCase property names, or convert from a PascalCase JObject to a camelCase JObject. For the final option, a provided an extension method and unit tests to demonstrate the behaviour.


Viewing all articles
Browse latest Browse all 743

Trending Articles