I am writing a RESTful web service using Java and Jersey, where the service will accept either XML or JSON inputs. Jackson is used as the JSON deserializer, and integrated into the Jersey config.
One of the endpoints is a POST request to a URL, where the content can be one of several different Java classes, and there is a common base class. These classes – with XML annotations – are:
@XmlRootElement(name = "action")
@XmlAccessorType(XmlAccessType.NONE)
@XmlSeeAlso({ FirstAction.class, SecondAction.class, ThirdAction.class })
public abstract class BaseAction {
}
@XmlRootElement(name = "first-action")
@XmlAccessorType(XmlAccessType.NONE)
public class FirstAction extends BaseAction implements Serializable {
}
// Likewise for SecondAction, ThirdAction
In my resource I can declare a method like:
@POST
@Path("/{id}/action")
public Response invokeAction(@PathParam("id") String id, BaseAction action) {...}
Then I can POST an XML fragment that looks like <firstAction/> and my method will be invoked with a FirstAction instance. So far so good.
Where I’m struggling is getting the JSON deserialization to work as seamlessly as the XML deserialization. Where the @XmlSeeAlso annotation was critical to get the XML deserialization working properly, it seemed that the equivalent for JSON was @JsonSubTypes. So I annotated the classes like this:
// XML annotations removed for brevity, but they are present as in the previous code snippet
@JsonSubTypes({ @JsonSubTypes.Type(name = "first-action", value = FirstAction.class),
@JsonSubTypes.Type(name = "second-action", value = SecondAction.class),
@JsonSubTypes.Type(name = "third-action", value = ThirdAction.class) })
public abstract class BaseAction {
}
@JsonRootName("first-action")
public class FirstAction extends BaseAction implements Serializable {
}
// Likewise for SecondAction, ThirdAction
I then feed it my test input: { "first-action": null } but all I can get is:
“org.codehaus.jackson.map.JsonMappingException: Root name ‘first-action’ does not match expected (‘action’) for type [simple type, class com.alu.openstack.domain.compute.server.actions.BaseAction]”
Unfortunately since I’m trying to be compatible with someone else’s API I can’t change my sample input – { "first-action": null } has to work, and deliver to my method an object of class FirstAction. (The action doesn’t have any fields, which is why null shouldn’t be a problem – it’s the type of the class that’s important).
What’s the correct way to have the JSON deserialization work in the same way as the XML deserialization already is?
I investigated the use of
@JsonTypeInfobut ran into problems because I could not alter the input format. The parser absolutely had to be able to handle input{ "first-action":null }. This ruled out the possibility of adding an@typeor@classproperty. Using a wrapper object may have worked, but it choked on thenullpayload.A crucial point was that I was using the UNWRAP_ROOT_PROPERTY configuration option. Jackson was absolutely insisting on finding an
actionproperty and I could not get it to consider anything else. So, I had to selectively disable UNWRAP_ROOT_PROPERTY for certain domain objects, so that Jackson would be open to parsing alternatives. I modified the project’s ContextResolver.getContext(…) implementation to check for a@JsonRootNameannotation – since this only has meaning if wrapping is enabled, I used the presence of this annotation to determine whether to return an object mapper configured with root property wrapping on, or off.At this stage, I might have been able to use
@JsonTypeInfo(include=JsonTypeInfo.As.WRAPPER_OBJECT, ...), except for the issue with thenullpayload mentioned above (this is used to indicate that the child object has no properties – if the spec I was working from had given an empty object {} instead then there would not be a problem). So to proceed I needed a custom type resolver.I created a new class that extended
org.codehaus.jackson.map.TypeDeserializer, with the purpose that whenever Jackson is called to deserialize aBaseActioninstance, it will call this custom deserializer. The deserializer will be given a subtypes array, which for BaseAction mapsfirst-action,second-action, etc. to FirstAction.class, etc. The deserializer reads the input stream for the field name, then matches the name to a class. If the next token is an object, then it finds and delegates to the appropriate deserializer for that class, or if it is null it finds the no-args constructor and invokes it to get an object.A class that implements org.codehaus.jackson.map.jsontype.TypeResolverBuilder is needed that can build an instance of this previous class, and then the TypeResolverBuilder is given as a
@JsonTypeResolverannotation on theBaseActionclass.