C# DataContracts – Using Subclasses for DataContracts with JSON

cjson

I have a RESTful call that can modify the state of an instance. My problem is that the JSON is different based on the type of action the user is taking, it could be a SetProperty action or a SetState action (among others).

For example, a POST /api/sample/ _would send the following JSON

{ 
  instanceId: 12312,      
  actionId: 12345, // Maps to a SetState action
  desiredState: 'CURATION'
}

But when calling the same URL for a SetProperty, the JSON would look like

{ 
  instanceId: 12312,      
  actionId: 25354, // Maps to a SetProperty action
  propName: "PropA",
  propValue: "user-input" 
}

I use the following data contract

[DataContract]
class PostInstanceRequest {
    [DataMember]
    int instanceId;
    [DataMember]
    int actionId;
    [DataMember]
    string desiredState;  // Only used for SetState
    [DataMember]
    string propName;   // Only used for SetProperty
    [DataMember]
    string propValue;  // Only used for SetProperty
    // Many more action specific properties here...
}

I don't really like this because it feels like I should have a base class for the common properties, and then I should define more base classes with the specific properties.

  • SetStatePostInstanceRequest, that adds desiredState
  • SetPropertyPostInstanceRequest, that adds propName and propValue

However, if I do that, I don't know what class to deserialize it into. I would have to first deserialize as a PostInstanceRequest, check the actionId property, and then deserialize it to the correct subclass.

A possible compromise is to use composition

[DataContract]
class SetStateAction {
    [DataMember]
    string desiredState;
}

[DataContract]
class SetPropAction {
    [DataMember]
    string propName;
    [DataMember]
    string propValue;
}

[DataContract]
class PostInstanceRequest {
    [DataMember]
    int instanceId;
    [DataMember]
    int actionId;
    [DataMember]
    SetPropAction setPropParams;
    [DataMember]
    SetStateAction setActionParams;
    // More references to action specific classes down here
}

Which of the options is going to cause me less trouble? Or is there an even better way that I didn't think of?

Best Answer

Did you author the service which you are posting to? If so, I would recommend changing it, so that you post SetState data to something like /api/sample/State, and post SetProperty data to /api/sample/Property. Then you would have separate controller actions for each service method, with each receiving only the desired parameters. If the service isn't yours and you can't modify it however, then obviously you're a bit stuffed. Otherwise, read this: http://www.asp.net/web-api/overview/web-api-routing-and-actions/routing-and-action-selection

Update

If you definitely want to retain a single action, at the same address, there is something you can do. You can define your controller action to take an interface / base class-typed parameter, and have the json deserializer use type information which you provide in your json to determine which class to rehydrate with your data, for example:

public abstract class ThingBase
{
    public int instanceId { get ; set; }
    public int actionId { get; set; }
}

public class SetStateThing : ThingBase
{
    public string desiredState { get; set; }
}

public class SetPropertyThing : ThingBase
{
    public string propName { get; set; }
    public string propValue { get; set; }
}

public class MyController
{
    public HttpResponseMessage Post([FromBody]ThingBase thing)
    {
        //use thing
    }
}

Your json would become, for example:

{
$type: "MyAssembly.SetStateThing, MyAssembly",
instanceId: 12312,      
actionId: 12345, // Maps to a SetState action
desiredState: "CURATION"
}

{
$type: "MyAssembly.SetPropertyThing, MyAssembly",
instanceId: 12312,      
actionId: 25354, // Maps to a SetProperty action
propName: "PropA",
propValue: "user-input" 
}

The trick is to set up the serializer settings correctly (in your WebApiConfig.cs Register(HttpConfiguration config) method for example):

config.Formatters.JsonFormatter.SerializerSettings.TypeNameHandling = TypeNameHandling.Objects;

I used this with an interface, but an abstract base class seems a better fit for your scenario.

Related Topic