How to Deserialize a Child Object with Dynamic (Numeric) Key Names

How to deserialize a child object with dynamic (numeric) key names?

You have a couple problems:

  • Your JSON has an extra level of nesting, with the root object containing a single property "users":

    {
    "users" : { ... }
    }

    Your data model needs to reflect this.

  • Your "users" object has a mixture of known and unknown property names. The question Deserialize json with known and unknown fields addresses a similar situation, however in your case your unknown properties always have a fixed schema and their values should be deserialized into a dictionary of POCOs -- specifically the User class. Therefore the answers there don't quite meet your needs, nor does the build-in functionality [JsonExtensionData].

The following converter allows for unknown properties to be deserialized into a typed container, rather than into an dictionary of arbitrary types:

[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false)]
public class JsonTypedExtensionDataAttribute : Attribute
{
}

public class TypedExtensionDataConverter<TObject> : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return typeof(TObject).IsAssignableFrom(objectType);
}

JsonProperty GetExtensionJsonProperty(JsonObjectContract contract)
{
try
{
return contract.Properties.Where(p => p.AttributeProvider.GetAttributes(typeof(JsonTypedExtensionDataAttribute), false).Any()).Single();
}
catch (InvalidOperationException ex)
{
throw new JsonSerializationException(string.Format("Exactly one property with JsonTypedExtensionDataAttribute is required for type {0}", contract.UnderlyingType), ex);
}
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
if (reader.TokenType == JsonToken.Null)
return null;
var jObj = JObject.Load(reader);
var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(objectType);
var extensionJsonProperty = GetExtensionJsonProperty(contract);

var extensionJProperty = (JProperty)null;
for (int i = jObj.Count - 1; i >= 0; i--)
{
var property = (JProperty)jObj.AsList()[i];
if (contract.Properties.GetClosestMatchProperty(property.Name) == null)
{
if (extensionJProperty == null)
{
extensionJProperty = new JProperty(extensionJsonProperty.PropertyName, new JObject());
jObj.Add(extensionJProperty);
}
((JObject)extensionJProperty.Value).Add(property.RemoveFromLowestPossibleParent());
}
}

var value = existingValue ?? contract.DefaultCreator();
using (var subReader = jObj.CreateReader())
serializer.Populate(subReader, value);
return value;
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
var contract = (JsonObjectContract)serializer.ContractResolver.ResolveContract(value.GetType());
var extensionJsonProperty = GetExtensionJsonProperty(contract);

JObject jObj;
using (new PushValue<bool>(true, () => Disabled, (canWrite) => Disabled = canWrite))
{
jObj = JObject.FromObject(value, serializer);
}

var extensionValue = (jObj[extensionJsonProperty.PropertyName] as JObject).RemoveFromLowestPossibleParent();
if (extensionValue != null)
{
for (int i = extensionValue.Count - 1; i >= 0; i--)
{
var property = (JProperty)extensionValue.AsList()[i];
jObj.Add(property.RemoveFromLowestPossibleParent());
}
}

jObj.WriteTo(writer);
}

[ThreadStatic]
static bool disabled;

// Disables the converter in a thread-safe manner.
bool Disabled { get { return disabled; } set { disabled = value; } }

public override bool CanWrite { get { return !Disabled; } }

public override bool CanRead { get { return !Disabled; } }
}

public struct PushValue<T> : IDisposable
{
Action<T> setValue;
T oldValue;

public PushValue(T value, Func<T> getValue, Action<T> setValue)
{
if (getValue == null || setValue == null)
throw new ArgumentNullException();
this.setValue = setValue;
this.oldValue = getValue();
setValue(value);
}

#region IDisposable Members

// By using a disposable struct we avoid the overhead of allocating and freeing an instance of a finalizable class.
public void Dispose()
{
if (setValue != null)
setValue(oldValue);
}

#endregion
}

public static class JsonExtensions
{
public static TJToken RemoveFromLowestPossibleParent<TJToken>(this TJToken node) where TJToken : JToken
{
if (node == null)
return null;
var contained = node.AncestorsAndSelf().Where(t => t.Parent is JContainer && t.Parent.Type != JTokenType.Property).FirstOrDefault();
if (contained != null)
contained.Remove();
// Also detach the node from its immediate containing property -- Remove() does not do this even though it seems like it should
if (node.Parent is JProperty)
((JProperty)node.Parent).Value = null;
return node;
}

public static IList<JToken> AsList(this IList<JToken> container) { return container; }
}

Then use it in your classes as follows:

class RootObject
{
[JsonProperty("users")]
public Users Users { get; set; }
}

[JsonConverter(typeof(TypedExtensionDataConverter<Users>))]
class Users
{
public Users()
{
this.UserTable = new Dictionary<string, User>();
}

[JsonProperty("parentname")]
public string ParentName { get; set; }

[JsonTypedExtensionData]
public Dictionary<string, User> UserTable { get; set; }
}

class User
{
public string name { get; set; }
public string state { get; set; }
public string id { get; set; }
}

I wrote the converter in a fairly general way so it can be reused. A converter that is hardcoded for the Users type would require less code.

Deserialize JSON with dynamic keys using C# Json.NET

The typical solution to dealing with dynamic keys is to use a Dictionary<string, T> in place of a regular class. See How can I deserialize a child object with dynamic (numeric) key names? for an example of this. However, that solution doesn't really work for your case, because there are other properties in the same object which do not have dynamic keys (the bankName and totalAmount), and the values of those properties are primitives whereas the value of dynamic property is an array of bank accounts. A better solution here is to use a JsonConverter.

Before we get to that, we need to set up a class structure to deserialize into. This is pretty straightforward:

class RootObject
{
public List<Bank> BankDetails { get; set; }
}

[JsonConverter(typeof(BankConverter))]
class Bank
{
public string BankName { get; set; }
public decimal TotalAmount { get; set; }
public List<Account> Accounts { get; set; }
}

class Account
{
[JsonProperty("sNo")]
public int SequenceNumber { get; set; }
[JsonProperty("acNo")]
public string AccountNumber { get; set; }
[JsonProperty("acBalance")]
public decimal Balance { get; set; }
}

You'll notice that I've added a few [JsonProperty] attributes in the Account class to map the shorthand property names in the JSON to friendlier property names in that class. And the [JsonConverter] attribute on the Bank class tells the serializer that we will be using a custom BankConverter to handle that class.

Here is the code for the BankConverter. It uses a JObject internally to make it easier to read and work with the JSON.

class BankConverter : JsonConverter
{
public override bool CanConvert(Type objectType)
{
return objectType == typeof(Bank);
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
JObject obj = JObject.Load(reader);
Bank bank = new Bank();
// populate the known properties (bankName and totalAmount)
serializer.Populate(obj.CreateReader(), bank);
// now handle the dynamic key
bank.Accounts = obj[bank.BankName].ToObject<List<Account>>(serializer);
return bank;
}

public override bool CanWrite
{
get { return false; }
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}

With these classes in place, you can deserialize the JSON like this:

var root = JsonConvert.DeserializeObject<RootObject>(json);

Here is a working demo: https://dotnetfiddle.net/knsRLv

How to deserialize JSON with dynamic and static key names in C#

You have two options, the first is to deserialise directly to a List<Dictionary<string, object>>, for example:

var responses = JsonConvert.DeserializeObject<List<Dictionary<string, object>>>(json);

Alternatively, if you are stuck on using your object, you will need to write a custom converter. For example, something like this:

public class MyResponseConverter : JsonConverter
{
public override bool CanConvert(Type type) => type == typeof(MyResponse);

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var responseObject = JObject.Load(reader);

MyResponse response = new MyResponse
{
StartTime = (string)responseObject["starttime"],
EndTime = (string)responseObject["endtime"],
};

var varData = new Dictionary<string, object>();

foreach (var property in responseObject.Properties())
{
if(property.Name == "starttime" || property.Name == "endtime")
{
continue;
}

varData.Add(property.Name, property.Value);
}

response.VarData = varData;
return response;
}

public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
// If you want to write to JSON, you will need to implement this method
throw new NotImplementedException();
}
}

And your class would change slightly to this:

[JsonConverter(typeof(MyResponseConverter))]
public class MyResponse
{
[JsonProperty(PropertyName = "starttime")]
public string StartTime { get; set; }
[JsonProperty(PropertyName = "endtime")]
public string EndTime { get; set; }

public Dictionary<string, object> VarData { get; set; }
}

Now you deserialise like this:

var responses = JsonConvert.DeserializeObject<List<MyResponse>>(json);

JSON to model with dynamic numeric property

The easiest thing you can do is just use Dictionary<,>, eg. Dictionary<string, DataPosition>.

Assuming you made a typo in the model and second class should be named DataPosition and ignoring the structure of DataPositions, the model would look like this:

public class Data
{
[JsonProperty("data_positions")]
public Dictionary<string, DataPosition> DataPositions { get; set; }
}

public class DataPosition
{
[JsonProperty("id")]
public int Id { get; set; }

[JsonProperty("name")]
public string Name { get; set; }

[JsonProperty("numberOfPos")]
public int NumberOfPos { get; set; }

[JsonProperty("partners")]
public Dictionary<string, Partner> Partners { get; set; }
}

public class Partner
{
[JsonProperty("id")]
public int Id { get; set; }

[JsonProperty("name")]
public string Name { get; set; }

[JsonProperty("numberOfPos")]
public int NumberOfPos { get; set; }
}

You can just access the entry in the dictionary by key, use .First() or iterate through them, depending on the data.

PS. The structure for the Partner type does not match the JSON, you might want to fix that.

How to deserialize root object with meaningless dynamic key names? Json.NET

You do not need to include an index. The songs are indexed by virtue of the fact that they will be deserialized to a collection. Change your schema like so:

{  
"version":"1.1",
"date":"2017-01-06",
"count":"130",
"songs":[
{
"artist":"Artist 1",
"title":"Title 1"
},
{
"artist":"Artist 2",
"title":"Title 2"
},
{
"artist":"Artist 3",
"title":"Title 3"
}
]
}

public class Song
{
public string artist {get; set;}
public string title {get; set;}
}

public class SongsResponse
{
public string version { get; set; }
public string date { get; set; }
public string count {get; set; }
public List<Song> songs { get; set; }
}

Update

So you may have no control over the json being sent to you but you still have control over your local domain objects and how you deserialize the json. I would stick with the class definitions I defined above and create a custom JsonConverter that you can use to deserialize the response into something that makes a little more sense. Actually, while you are at it you can make your SongsResponse class 'better' by using the appropriate type for the date and count properties (why use a strongly typed language if you are not going to take advantage of it).

So this class:

public class SongsResponseJsonConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException("Unnecessary because CanWrite is false. The type will skip the converter.");
}

public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
var result = new SongsResponse();

var obj = JObject.Load(reader);
result.version = obj["version"].Value<string>();
result.date = obj["date"].Value<DateTime>();
result.count = obj["count"].Value<int>();
result.songs =
obj
.Properties()
.Where(x => !(new string[] { "version", "date", "count" }.Contains(x.Name)))
.Select(x => new { Id = int.Parse(x.Name), Song = x })
.OrderBy(x => x.Id)
.Select(x => x.Song.Value.ToObject<Song>())
.ToList();
return result;
}

public override bool CanRead
{
get { return true; }
}

public override bool CanWrite
{
get { return false; }
}

public override bool CanConvert(Type objectType)
{
return objectType == typeof(SongsResponse);
}
}

Which you would use like this:

var json = @"
{
""version"": ""1.1"",
""date"": ""2017-01-06"",
""count"": ""130"",
""0"": {
""artist"": ""Artist 1"",
""title"": ""Title 1""
},
""1"": {
""artist"": ""Artist 2"",
""title"": ""Title 2""
},
""49"": {
""artist"": ""Artist 50"",
""title"": ""Title 50""
}
}";

var response = JsonConvert.DeserializeObject<SongsResponse>(json, new SongsResponseJsonConverter());


Related Topics



Leave a reply



Submit