Search code examples
c#json.netjson-deserializationsystem.text.json

Deserializing a JSON with nested dynamic property names


I'm trying to deserialize a pretty ugly JSON provided by an external REST API and am wondering about the "proper" way to do that (I'm using System.Text.Json in .net 6). Details follow:

I have a model for the data:

class DeviceData{
    //lots of properties
}

which works fine (i.e I can just JsonSerializer.Deserialize<DeviceData> the response) when making an API query for a single instance, since it returns a nice JSON one would expect:

{
    "property1_name": value,
    "property2_name": value,
    ...
}

The problem begins when I use the batch query provided by the API, since the response to api_url/batch?=device1,device2,... looks as if someone failed to make an array (the device1s are alphanumeric strings pulled form a database) is:

{
"names":[
   "device1",
   "device2",
   ...
   ],
"device1":{
   "stuff_i_dont_need": value,
   "device1": {
       "property1_name": value,
       "property2_name": value,
        ...
    }
 }
 "device2":{
     ...
 }
 ...
}

The double nesting of dynamic property names means I can't just deserialize the second response as a dictionary of <string, myclass> pairs. I managed to hack something together using JsonDocument but it's extremly ugly and it feels like there should be a nice short way to do that with just JsonSerializer and maybe some reader overrides.


Solution

  • Using Deserialize subsections of a JSON payload from How to use a JSON document, Utf8JsonReader, and Utf8JsonWriter in System.Text.Json as template you could do something like this:

    JsonNode root = JsonNode.Parse(json)!;
    
    Dictionary<string, X> devices = new();
    foreach(string name in root["names"]!.AsArray()) {
        var o = root[name][name].AsObject();
        using var stream = new MemoryStream();
        using var writer = new Utf8JsonWriter(stream);
        o.WriteTo(writer);
        writer.Flush();
        X? x = JsonSerializer.Deserialize<X>(stream.ToArray());
    
        var innerJson = root[name][name].ToJsonString();
        devices[name] = x;
    }
    
    foreach(var d in devices) Console.WriteLine($"{d.Key}: {d.Value}");
    

    This prints

    device1: X { property1_name = 12, property2_name = 13 }
    device2: X { property1_name = 22, property2_name = 23 }
    

    I'm not sure if this is faster/better than calling ToJsonString():

    JsonNode root = JsonNode.Parse(json)!;
    
    Dictionary<string, X> devices = new();
    foreach(string name in root["names"]!.AsArray()) {
        var innerJson = root[name][name].ToJsonString();
        devices[name] = JsonSerializer.Deserialize<X>(innerJson);
    }
    
    foreach(var d in devices) Console.WriteLine($"{d.Key}: {d.Value}")
    
    

    If you're after fancy you could go full LINQ:

    JsonNode root = JsonNode.Parse(json)!;
    
    Dictionary<string, X> devices = root["names"]!.AsArray()
        .Select(name => (string)name)
        .ToDictionary(
            keySelector: name => name, 
            elementSelector: name => System.Text.Json.JsonSerializer.Deserialize<X>(root[name][name].ToJsonString()));
    
    foreach(var d in devices) Console.WriteLine($"{d.Key}: {d.Value}");
    

    Both print