Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Convert JSON camel case to snake case (and vice versa) and stringify numeric values

I have to send and receive JSON objects to a web REST service. The objects are produced by a DLL that serializes the attribute names in upper camel case ("PropertyName"), and the web service wants snake case ("property_name"). Plus, the DLL serializes numeric values as floating point numbers, but the REST APIs want all strings. After processing the object, the REST service returns snake case JSON.

The JSON is complex and contains nested arrays and objects. While converting back from the REST string, I can skip the de-stringification of the numeric strings, but I still have to reconvert the attribute names to upper camel case.

I was thinking of writing a helper class using the Newtonsoft Json library, but it looks trickier than I expected. The converter should accept JSON and return JSON.

Example:

{
    "FirstObject": {
        "NestedObject": {
            "AttributeString": "ok",
            "AttributeNumeric": 123.45
        },
        "OtherObject": [{
            "ArrayVal": 100
        }, {
            "ArrayVal": 200
        }]
    }
}

should become

{
    "first_object": {
        "nested_object": {
            "attribute_string": "ok",
            "attribute_numeric": "123.45"
        },
        "other_object": [{
            "array_val": "100"
        }, {
            "array_val": "200"
        }]
    }
}

I see that the Json.Net library has SnakeCaseNamingStrategy and CamelCaseNamingStrategy classes, so the idea was using a JsonTextReader to parse the input, change the naming convention of the property names, set numeric values as string, and write the modified tokens using a JsonTextWriter.

I could not find any sample on how to do this.

like image 944
davidthegrey Avatar asked Mar 04 '23 21:03

davidthegrey


1 Answers

The easiest way to do what you want is to use a set of model classes that match your JSON. (You can generate the classes in Visual Studio by copying a complete sample of the JSON to the clipboard and then using the Edit -> Paste Special -> Paste JSON as Classes function.) Make the model classes use upper camel case for the property names (which is the standard naming convention of C# anyway), and use strings in place of numeric properties.

So, for your example JSON, the model classes would look like this:

public class RootObject
{
    public FirstObject FirstObject { get; set; }
}

public class FirstObject
{
    public NestedObject NestedObject { get; set; }
    public List<OtherObject> OtherObject { get; set; }
}

public class NestedObject
{
    public string AttributeString { get; set; }
    public string AttributeNumeric { get; set; }
}

public class OtherObject
{
    public string ArrayVal { get; set; }  // using string instead of int here
}

Then, to convert from the upper camel case JSON to snake case, you can do this:

var obj = JsonConvert.DeserializeObject<RootObject>(json);
var settings = new JsonSerializerSettings
{
    ContractResolver = new DefaultContractResolver
    {
        NamingStrategy = new SnakeCaseNamingStrategy { ProcessDictionaryKeys = true }
    },
    Formatting = Formatting.Indented
};
json = JsonConvert.SerializeObject(obj, settings);

The original JSON will deserialize to the model naturally because the property names already match. Json.Net will automatically convert the numeric values in the JSON to strings as needed to fit the class properties. On serialization, the SnakeCaseNamingStrategy comes into play to change the property names to snake case. The numeric values are written out as strings because that's the way the properties are declared in the classes.

To go back the other way, you would do:

obj = JsonConvert.DeserializeObject<RootObject>(json, settings);  // same settings as above
json = JsonConvert.SerializeObject(obj, Formatting.Indented);

Here, during deserialization, Json.Net uses the SnakeCaseNamingStrategy to convert the model property names to snake case again to match them up with the JSON properties. The numeric values are already strings in the JSON so there's no conversion needed. On serialization, we aren't using any special settings, so the property names are written out exactly as declared, which is upper camel case. The string properties holding numeric values remain strings (you said this was OK in your question).

Here is a round-trip demo: https://dotnetfiddle.net/3Pb1fT


If you don't have a model to work with, it's still possible to do this conversion using the JsonReader/JsonWriter approach you suggested, but it will take a little more code to glue them together and do the transformations. Here is a helper method that will do the bulk of the heavy lifting:

public static void ConvertJson(TextReader textReader, TextWriter textWriter, 
                               NamingStrategy strategy, 
                               Formatting formatting = Formatting.Indented)
{
    using (JsonReader reader = new JsonTextReader(textReader))
    using (JsonWriter writer = new JsonTextWriter(textWriter))
    {
        writer.Formatting = formatting;
        if (reader.TokenType == JsonToken.None)
        {
            reader.Read();
            ConvertJsonValue(reader, writer, strategy);
        }
    }
}

private static void ConvertJsonValue(JsonReader reader, JsonWriter writer, 
                                     NamingStrategy strategy)
{
    if (reader.TokenType == JsonToken.StartObject)
    {
        writer.WriteStartObject();
        while (reader.Read() && reader.TokenType != JsonToken.EndObject)
        {
            string name = strategy.GetPropertyName((string)reader.Value, false);
            writer.WritePropertyName(name);
            reader.Read();
            ConvertJsonValue(reader, writer, strategy);
        }
        writer.WriteEndObject();
    }
    else if (reader.TokenType == JsonToken.StartArray)
    {
        writer.WriteStartArray();
        while (reader.Read() && reader.TokenType != JsonToken.EndArray)
        {
            ConvertJsonValue(reader, writer, strategy);
        }
        writer.WriteEndArray();
    }
    else if (reader.TokenType == JsonToken.Integer)
    {
        // convert integer values to string
        writer.WriteValue(Convert.ToString((long)reader.Value));
    }
    else if (reader.TokenType == JsonToken.Float)
    {
        // convert floating point values to string
        writer.WriteValue(Convert.ToString((double)reader.Value,
                          System.Globalization.CultureInfo.InvariantCulture));        
    }
    else // string, bool, date, etc.
    {
        writer.WriteValue(reader.Value);
    }
}

To use it, you just need to set up a TextReader for your input JSON and a TextWriter for the output, and pass in the appropriate NamingStrategy you want to use for the conversion. For example, to convert your original JSON string to snake case, you would do this:

using (StringReader sr = new StringReader(upperCamelCaseJson))
using (StringWriter sw = new StringWriter())
{
    ConvertJson(sr, sw, new SnakeCaseNamingStrategy(), formatting);
    string snakeCaseJson = sw.ToString();
    ...
}

Or, if the source and/or destination for your JSON is a stream of some sort, you can use a StreamReader/StreamWriter instead:

using (StreamReader sr = new StreamReader(inputStream))
using (StreamWriter sw = new StreamWriter(outputStream))
{
    ConvertJson(sr, sw, new SnakeCaseNamingStrategy(), formatting);
}

Now, for the return trip, there is bit of an issue. A NamingStrategy only works in one direction; it doesn't provide a facility for reversing the conversion. That means none of the Newtonsoft-supplied NamingStrategy classes will work for converting snake case back to upper camel case the way you want. The CamelCaseNamingStrategy won't work because it doesn't expect to start with snake case (it wants upper camel case), and its output isn't upper camel anyway. The DefaultNamingStrategy won't work either, because it doesn't actually do any conversion at all--it's just a pass-through.

The solution is to make your own custom NamingStrategy. Fortunately this isn't difficult to do. Just derive from the base NamingStrategy class and implement the abstract ResolvePropertyName method:

// This naming strategy converts snake case names to upper camel case (a.k.a. proper case)
public class ProperCaseFromSnakeCaseNamingStrategy : NamingStrategy
{
    protected override string ResolvePropertyName(string name)
    {
        StringBuilder sb = new StringBuilder(name.Length);
        for (int i = 0; i < name.Length; i++)
        {
            char c = name[i];

            if (i == 0 || name[i - 1] == '_')
                c = char.ToUpper(c);

            if (c != '_')
                sb.Append(c);
        }
        return sb.ToString();
    }
}

Now you can pass this new strategy to the ConvertJson method as described above to convert the snake case JSON back to upper camel case.

Round-trip demo: https://dotnetfiddle.net/jt0XKD

like image 103
Brian Rogers Avatar answered Apr 05 '23 23:04

Brian Rogers