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.

Leave a Comment