Richard
Richard

Reputation: 30628

Deserialize JSON to multiple properties

I am programming against a third party API which returns JSON data, but the format can be a little strange. Certain properties can either be an object (which contains an Id property), or a string (which is the Id of the object). For example, both of the following are valid:

{
    ChildObject: 'childobjectkey1'
}

and

{
    ChildObject: {
        Id: 'childobjectkey1',
        // (other properties)
    }
}

I'm trying to deserialize this using JSON.net into a strongly typed class, but haven't had much luck so far. My best idea was to serialise it to two properties, one a string and the other an object, and to use a custom JsonConverter for each to allow for the variable behaviour:

public abstract class BaseEntity
{
    public string Id { get; set; }
}

public class ChildObject : BaseEntity { }

public class MyObject
{
    [JsonProperty("ChildObject")]
    [JsonConverter(typeof(MyCustomIdConverter))]
    public string ChildObjectId { get; set; }

    [JsonProperty("ChildObject")]
    [JsonConverter(typeof(MyCustomObjectConverter))]
    public ChildObject ChildObject { get; set; }
}

However, setting the JsonProperty attribute on two properties with the same PropertyName causes the exception:

Newtonsoft.Json.JsonSerializationException: A member with the name 'ChildObject' already exists on '.....'. Use the JsonPropertyAttribute to specify another name.

I'm fairly sure the JsonConverter approach will work if I can get over this hurdle - I suspect the error is there because the JsonProperty attribute is used for Serialization as well as Deserialization. In this instance I have no interest in Serializing this class - it will only ever be used as the target for Deserialization.

I have no control over the remote end (it's a third party API), but I would like to be able to achieve this deserialisation. I don't mind if it's using the approach I've started on, or one I've not thought of yet.

This question is also related, but there were no answers.

Upvotes: 16

Views: 11889

Answers (3)

Brian Rogers
Brian Rogers

Reputation: 129827

Here is what I would do in this situation.

  • Only have a single property in your parent class for the child object, and make it of type ChildObject
  • Create a custom JsonConverter which can inspect the JSON and either:
    • deserialize the full instance of the child object if the data is present, or
    • create a new instance of the child object and set its ID, leaving all other properties blank. (Or you could do as Jeff Mercado suggested and have the converter load the object from a database based on the ID, if that applies to your situation.)
  • Optionally, put a property on the child object indicating whether it is fully populated. The converter can set this property during deserialization.

After deserialization, if there was a ChildObject property in the JSON (with either an ID or a full object value), you are guaranteed to have a ChildObject instance and you can get its ID from it; otherwise, if there was no ChildObject property in the JSON, the ChildObject property in the parent class will be null.

Below is a full working example to demonstrate. In this example, I modified the parent class to include three separate instances of the ChildObject to show the different possibilities in the JSON (string ID only, full object and neither present). They all use the same converter. I also added a Name property and an IsFullyPopulated property to the ChildObject class.

Here are the DTO classes:

public abstract class BaseEntity
{
    public string Id { get; set; }
}

public class ChildObject : BaseEntity 
{
    public string Name { get; set; }
    public bool IsFullyPopulated { get; set; }
}

public class MyObject
{
    [JsonProperty("ChildObject1")]
    [JsonConverter(typeof(MyCustomObjectConverter))]
    public ChildObject ChildObject1 { get; set; }

    [JsonProperty("ChildObject2")]
    [JsonConverter(typeof(MyCustomObjectConverter))]
    public ChildObject ChildObject2 { get; set; }

    [JsonProperty("ChildObject3")]
    [JsonConverter(typeof(MyCustomObjectConverter))]
    public ChildObject ChildObject3 { get; set; }
}

Here is the converter:

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

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        JToken token = JToken.Load(reader);
        ChildObject child = null;
        if (token.Type == JTokenType.String)
        {
            child = new ChildObject();
            child.Id = token.ToString();
            child.IsFullyPopulated = false;
        }
        else if (token.Type == JTokenType.Object)
        {
            child = token.ToObject<ChildObject>();
            child.IsFullyPopulated = true;
        }
        else if (token.Type != JTokenType.Null)
        {
            throw new JsonSerializationException("Unexpected token: " + token.Type);
        }
        return child;
    }

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        serializer.Serialize(writer, value);
    }
}

Here is the test program to demonstrate the operation of the converter:

class Program
{
    static void Main(string[] args)
    {
        string json = @"
        {
            ""ChildObject1"": 
            {
                ""Id"": ""key1"",
                ""Name"": ""Foo Bar Baz""
            },
            ""ChildObject2"": ""key2""
        }";

        MyObject obj = JsonConvert.DeserializeObject<MyObject>(json);

        DumpChildObject("ChildObject1", obj.ChildObject1);
        DumpChildObject("ChildObject2", obj.ChildObject2);
        DumpChildObject("ChildObject3", obj.ChildObject3);
    }

    static void DumpChildObject(string prop, ChildObject obj)
    {
        Console.WriteLine(prop);
        if (obj != null)
        {
            Console.WriteLine("   Id: " + obj.Id);
            Console.WriteLine("   Name: " + obj.Name);
            Console.WriteLine("   IsFullyPopulated: " + obj.IsFullyPopulated);
        }
        else
        {
            Console.WriteLine("   (null)");
        }
        Console.WriteLine();
    }
}

And here is the output of the above:

ChildObject1
   Id: key1
   Name: Foo Bar Baz
   IsFullyPopulated: True

ChildObject2
   Id: key2
   Name:
   IsFullyPopulated: False

ChildObject3
   (null)

Upvotes: 3

Jeff Mercado
Jeff Mercado

Reputation: 134611

Rather than creating two separate converters for each of the fields, it would be wise to create a single converter for the "main" property and link the other one to it. ChildObjectId is derived from the ChildObject.

public class MyObject
{
    [JsonIgnore]
    public string ChildObjectId
    {
        get { return ChildObject.Id; }

        // I would advise against having a setter here
        // you should only allow changes through the object only
        set { ChildObject.Id = value; }
    }

    [JsonConverter(typeof(MyObjectChildObjectConverter))]
    public ChildObject ChildObject { get; set; }
}

Now to convert the ChildObject can be a bit of a challenge. There are two possible representations of the object: a string or an object. Determine what representation you have and perform the conversion.

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

    public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
    {
        serializer.Serialize(writer, value);
    }

    public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
    {
        var obj = serializer.Deserialize<JToken>(reader);
        switch (obj.Type)
        {
        case JTokenType.Object:
            return ReadAsObject(obj as JObject);
        case JTokenType.String:
            return ReadAsString((string)(JValue)obj);
        default:
            throw new JsonSerializationException("Unexpected token type");
        }
    }

    private object ReadAsObject(JObject obj)
    {
        return obj.ToObject<ChildObject>();
    }

    private object ReadAsString(string str)
    {
        // do a lookup for the actual object or whatever here
        return new ChildObject
        {
            Id = str,
        };
    }
}

Upvotes: 6

Alex Filipovici
Alex Filipovici

Reputation: 32571

Try this (extend it with some thorough validation if you'll be using it in your code):

public class MyObject
{
    public ChildObject MyChildObject;
    public string MyChildObjectId;

    [JsonProperty("ChildObject")]
    public object ChildObject
    {
        get
        {
            return MyChildObject;
        }
        set
        {
            if (value is JObject)
            {
                MyChildObject = ((JToken)value).ToObject<ChildObject>();
                MyChildObjectId = MyChildObject.Id;
            }
            else
            {
                MyChildObjectId = value.ToString();
                MyChildObject = null;
            }
        }
    }
}

Upvotes: 9

Related Questions