Reputation: 276
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
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.
I added the CanWrite override as suggested there by Dribbel:
public override bool CanWrite
{
get { return false; }
}
I also added my own helper function to JsonCreationConverter:
protected bool FieldExists(string fieldName, JObject jObject) {
return jObject[fieldName] != null;
}
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();
}
}
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:
Upvotes: 0
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