Mykro
Mykro

Reputation: 276

Deserializing descendent objects

I'm communicating with a JSON-based API which I can't change. It always returns a Response object with a varying Result object inside. Typically it looks like this:

{ "ver": "2.0", "result": { "code": 0 } }

For certain commands the Result object is 'grown' by adding extra properties:

{ "ver": "2.0", "result": { "code": 0, "hostName": "sample", "hostPort": 5000 } }

I've used Newtonsoft attributes to define the objects as follows:

 internal class RpcResponse
    {
        [JsonProperty(PropertyName = "ver")]
        public string Version { get; set; }

        [JsonProperty(PropertyName = "result")]
        public RpcResponseResult Result
        {
            get;
            set;
        }

internal class RpcResponseResult
    {
        [JsonProperty(PropertyName = "code")]
        public int Code { get; set; }
    }

internal class RpcExtendedResponseResult: RpcResponseResult
    {
        [JsonProperty(PropertyName = "hostName")]
        public string HostName { get; set; }

        [JsonProperty(PropertyName = "hostPort")]
        public int HostPort { get; set; } 

But when the Response object is deserialized:

RpcResponse rspResponse = JsonConvert.DeserializeObject<RpcResponse>(rspString);

Its Result property always appears as an RpcResponseResult object, ie. JsonConvert doesn't know to construct it as a RpcExtendedResponseResult object.

Is there some way with Attributes or Converters to reinstate the correct descendent object? I feel like I'm missing something obvious!

Upvotes: 0

Views: 314

Answers (2)

Mykro
Mykro

Reputation: 276

First, credit to Matthew Frontino for providing the only answer which I've accepted.

However I opted not to make a single result container, so here's what I ended up doing.

  1. First I started with this page: How to implement custom JsonConverter in JSON.NET to deserialize a List of base class objects?
  2. I used the version of JsonCreationConverter provided there by Alain.
  3. I added the CanWrite override as suggested there by Dribbel:

    public override bool CanWrite
    {
        get { return false; }
    }
    
  4. I also added my own helper function to JsonCreationConverter:

    protected bool FieldExists(string fieldName, JObject jObject)    {
        return jObject[fieldName] != null;
    }
    
  5. Then I created my own converter as follows:

    class RpcResponseResultConverter : JsonCreationConverter<RpcResponseResult>
    {
        protected override RpcResponseResult Create(Type objectType, JObject jObject)
        {
            // determine extended responses
            if (FieldExists("hostName", jObject) &&
                FieldExists("hostPort", jObject) )
            {
                return new RpcExtendedResponseResult();
            }
            //default
            return new RpcResponseResult();
        }
    }
    
  6. Then I deserialize the top-level class and supply any converters to be used. In this case I only supplied one, which was for the nested class in question:

    RpcResponse rspResponse = JsonConvert.DeserializeObject<RpcResponse>(
        rspString, 
        new JsonSerializerSettings {
            DateParseHandling = Newtonsoft.Json.DateParseHandling.None,
            Converters = new List<JsonConverter>( new JsonConverter[] {
                    new RpcResponseResultConverter()
                    })
            });
    

Notes:

  • Anything not explicitly handled by a converter (such as the top-level class) is deserialized using the default converter built into JsonConvert.
  • This only works if you can identify a unique set of fields for every descendent class.

Upvotes: 0

Matthew Frontino
Matthew Frontino

Reputation: 496

It's because the type of the object is RpcResponseResult. The deserializer can only deserialize fields that are declared in the type of the field specified. It can't determine because a class has "hostName" its now an RpcExtendedResponseResult.

If I were doing this, I might make the result a container for all possible fields with default values if needed, and then you can fill another object as needed.

internal class RpcResponseResultContainer
{
    [JsonProperty(PropertyName = "code")]
    public int Code { get; set; }

    [JsonProperty(PropertyName = "hostName")]
    private string mHostName = string.Empty;
    public string HostName 
    { 
       get { return mHostName;} 
       set { mHostName = value; }
    }

    [JsonProperty(PropertyName = "hostPort")]
    private int mHostPort = -1;
    public int HostPort 
    { 
       get { return mHostPort;} 
       set { mHostPort = value;}
    }

Then if you really wanted to get your object as you want it, you could do something like this in your container class:

 public RpcResponseResult GetActualResponseType()
 {
     if(HostPort != -1 && !string.IsNullOrEmtpy(HostName))
     {
         return new RpcExtendedResponseResult() { Code = this.Code, HostName = this.HostName, HostPort = this.HostPort};
     }
     return new RpcResponseResult() { Code = this.Code };
 }

Upvotes: 1

Related Questions