skip to Main Content

I’m trying to convert JSON responses, in which all keys are in snake_case, for example

{
   "op": 0,
   "t": "some_name",
   "d": {
     "heartbeat_interval": 10000,
     "user": {
        "user_id": 1
     }
   }
}

(This is just one of the many things that d could be)

and this would need to be converted to a c# class, such as:

class Example {
   public int op { get; set; }
   public string t { get; set; }
   public dynamic d { get; set; } // What's in this cannot be predicted
}

Which could then be accessed using res.d.user.userId or res.d.heartbeatInterval for example.

I know this is possible with serialising camelCase TO snake_case but I can’t find any way to do it the other way around

I’ve tried looking on the newtonsoft documentation but I couldn’t find anything except from converting camelCase TO snake_case.

2

Answers


  1. Consider using [JsonProperty] attribute, it will indicate the name of the property you want to deserialize from.

    public class Example
    {
        [JsonProperty("example_key")]
        public dynamic exampleKey { get; set; }
        [JsonProperty("example_key3")]
        public int exampleKey3 { get; set; }
    }
    
    public void SnakeCaseToCamelCaseDeserialization()
    {
            var json = @"{
                           'example_key': {
                              'example_key_2': 'value'
                           },
                           'example_key3': 5
                        }";
    
            var example = JsonConvert.DeserializeObject<Example>(json);
    
            Console.WriteLine($"exampleKey: {example.exampleKey?.ToString()}");
            Console.WriteLine($"exampleKey3 : {example.exampleKey3}");
     }
    

    Alternatively you could omit using [JsonProperty] attribute and use Newtonsoft’s SnakeCaseNamingStrategy class in JsonSerializerSettings when deserializing.

    public class Example
    {
        public dynamic exampleKey { get; set; }
        public int exampleKey3 { get; set; }
    }
    
    public void SnakeCaseToCamelCaseDeserialization()
    {
            var json = @"{
                           'example_key': {
                              'example_key_2': 'value'
                           },
                           'example_key3': 5
                        }";
    
            var example = JsonConvert.DeserializeObject<Example>(
                json,
                new JsonSerializerSettings
                {
                    ContractResolver = new DefaultContractResolver { NamingStrategy = new SnakeCaseNamingStrategy() }
                });
    }
    
    Login or Signup to reply.
  2. When you declare a member to be of type object or dynamic, Json.NET will deserialize JSON objects and arrays as either JObject or JArray from the LINQ to JSON document object model. Since their base type JToken implements IDynamicMetaObjectProvider, you are able to query these types using dynamic syntax such as res.d.user.userId.

    Now, there is no built-in way to automatically rename properties as they are being loaded into a JToken hierarchy, but you could adopt some of the code from this answer to Get a dynamic object for JsonConvert.DeserializeObject making properties uppercase to create a custom converter that does this:

    public class SnakeToCamelCaseDynamicReadOnlyConverter : JsonConverter
    {
        public override bool CanConvert(Type objectType) => objectType == typeof(object);
    
        // TODO: This method removes all underscores, and uppercases letters that had a preceding underscore.
        // If you would prefer a different handling for leading and trailing underscores, or multiple underscores, modify to your tastes.
        static string SnakeToCamelCase(string s) =>
            s.IndexOf("_") < 0 ? s 
            : s.Select((c, i) => (c, i)).Where(p => p.c != '_').Select(p => p.i == 0 || s[p.i-1] != '_' ? p.c : char.ToUpperInvariant(p.c))
               .Aggregate(new StringBuilder(), (sb, c) => sb.Append(c))
               .ToString();
    
        public override object? ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) =>
            reader.Parse(SnakeToCamelCase) switch
            {
                JValue v => v.Value, // For primitive values return the underlying .NET primitive
                var t => t, // Otherwise return the JObject or JArray.
            };
    
        public override bool CanWrite => false;
        public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) => throw new NotImplementedException();
    }
    
    public static partial class JsonExtensions
    {
        // Taken from this answer https://stackoverflow.com/a/35784914/3744182
        // To https://stackoverflow.com/questions/35777561/get-a-dynamic-object-for-jsonconvert-deserializeobject-making-properties-uppercase
        public static JToken? Parse(this JsonReader reader, Func<string, string> nameMap)
        {
            using var writer = new RenamingJTokenWriter(nameMap);
            writer.WriteToken(reader);
            return writer.Token;
        }
        
        // Return the lowest JContainer parent that is not a JProperty
        public static JContainer? LowestParent(this JToken? token)
        {
            while (token != null && (token is not JContainer || token is JProperty))
                token = token.Parent;
            return (JContainer?)token;
        }
    }
    
    class RenamingJTokenWriter : JTokenWriter
    {
        readonly Func<string, string> nameMap;
    
        public RenamingJTokenWriter(Func<string, string> nameMap) : base() { this.nameMap = nameMap ?? throw new ArgumentNullException(nameof(nameMap)); }
    
        public override void WritePropertyName(string name) 
        {
            var newName = nameMap(name);
            if (CurrentToken.LowestParent() is JObject o && o.Property(newName, StringComparison.Ordinal) != null)
                throw new JsonException($"Duplicate name {newName}");
            base.WritePropertyName(newName);
        }
    
        // No need to override WritePropertyName(string name, bool escape) since it calls WritePropertyName(string name)
    }
    

    Next, modify Example as follows:

    class Example {
        public int op { get; set; }
        public string t { get; set; }
    
        [JsonConverter(typeof(SnakeToCamelCaseDynamicReadOnlyConverter))]
        public dynamic d { get; set; } // What's in this cannot be predicted
    }
    

    And you will be able to deserialize your JSON and perform dynamic queries using camel case as required:

    var res = JsonConvert.DeserializeObject<Example>(json);
    
    Console.WriteLine($"res.d.user.userId = {res.d.user.userId}"); // Prints 1 
    Console.WriteLine($"res.d.heartbeatInterval = {res.d.heartbeatInterval}"); // Prints 10000
    

    Notes:

    • The SnakeToCamelCase() method is merely a prototype that removes all underscores and uppercases any character that had been preceded by an underscore.

      If you would prefer smarter handling for property names with leading and trailing underscores, or sequences of multiple underscores (e.g. "_property_name__0001_" or "___" or whatever) modify the method to your tastes.

    • It’s possible you could get get name collisions when remapping from snake case, e.g.:

      {
          "heartbeat_interval": 10000,
          "heartbeatInterval": 20000,
      }
      

      The code above detects this situation and throws a JsonException. If you don’t want that you can remove the check from RenamingJTokenWriter.WritePropertyName(), but if you do, the deserialized object will contain the value of the final duplicate name encountered.

    Demo fiddle here.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search