Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JAXB and inheritance

Tags:

jaxb

I am trying to read a JSON file like:

{
  "a": "abc",
  "data" : {
      "type" : 1,
      ...
  }
}

where the ... part is replaceable based on the type like:

{
  "a": "abc",
  "data" : {
      "type" : 1,
      "b" : "bcd"
  }
}

or:

{
  "a": "abc",
  "data" : {
      "type" : 2,
      "c" : "cde",
      "d" : "def",
  }
}

For the life of me I cannot figure out the proper JAXB annotations/classes to use to make this happen. I don't have an issue moving the type variable outside of the data block if needed.

I'm using Glassfish 3.1.2.2.

Edit:

Based on the code provided by Perception I did a quick attempt... doesn't work in glassfish though:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = As.PROPERTY, property = "type")
@JsonSubTypes(
{
    @JsonSubTypes.Type(value = DataSubA.class, name = "1"),
    @JsonSubTypes.Type(value = DataSubB.class, name = "2") 
})
@XmlRootElement
public abstract class Data implements Serializable 
{
    private static final long serialVersionUID = 1L;

    public Data() 
    {
        super();
    }
}

@XmlRootElement
@XmlAccessorType(XmlAccessType.NONE)
public class DataSubA 
    extends Data 
{
    private static final long serialVersionUID = 1L;

    @XmlElement
    private BigDecimal expenditure;

    public DataSubA() {
        super();
    }

    public DataSubA(final BigDecimal expenditure) {
        super();
        this.expenditure = expenditure;
    }

    @Override
    public String toString() {
        return String.format("%s[expenditure = %s]\n", 
                             getClass().getSimpleName(), getExpenditure());
    }

    public BigDecimal getExpenditure() {
        return expenditure;
    }

    public void setExpenditure(BigDecimal expenditure) {
        this.expenditure = expenditure;
    }
}

@XmlRootElement
@XmlAccessorType(XmlAccessType.NONE)
public class DataSubB 
    extends Data 
{
    private static final long serialVersionUID = 1L;

    @XmlElement
    private String name;

    @XmlElement
    private Integer age;

    public DataSubB() 
    {
        super();
    }

    public DataSubB(final String name, final Integer age) 
    {
        super();
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() 
    {
        return String.format("%s[name = %s, age = %s]\n", 
                             getClass().getSimpleName(), getName(), getAge());
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }
}

@XmlRootElement
@XmlAccessorType(XmlAccessType.NONE)
public class DataWrapper
{ 
    @XmlElement
    private Data data;

    public Data getData() {
        return data;
    }

    public void setData(Data data) {
        this.data = data;
    }
}

And a simple POST that takes it in:

@Stateless
@Path("x")
public class Endpoint
{
    @POST
    @Consumes(
    {
        MediaType.APPLICATION_JSON,
    })
    @Produces(
    {
        MediaType.APPLICATION_JSON,
    })
    public String foo(final DataWrapper wrapper)
    {
        return ("yay");
    }
}

When I pass in JSON like:

{
    "data" : 
    {
        "type" : 1,
        "expenditure" : 1
    }
}

I get a message like:

Can not construct instance of Data, problem: abstract types can only be instantiated with additional type information
 at [Source: org.apache.catalina.connector.CoyoteInputStream@28b92ec1; line: 2, column: 5] (through reference chain: DataWrapper["data"])
like image 490
TofuBeer Avatar asked May 07 '13 22:05

TofuBeer


1 Answers

On the DataClass add an @XmlSeeAlso annotation that specifies all of the subclasses:

@XmlRootElement
@XmlSeeAlso({DataSubA.class, DataSubB.class})
public abstract class Data implements Serializable {

Then on each of the subclasses use the @XmlType annotation to specify the type name.

@XmlType(name="1")
public class DataSubA extends Data {

UPDATE

Note: I'm the EclipseLink JAXB (MOXy) lead and a member of the JAXB (JSR-222) expert group.

The JAXB (JSR-222) specification doesn't cover JSON-binding. There are different ways JAX-RS allows you to specify JSON mapping via JAXB annotations:

  1. A JAXB implementation plus a library like Jettison that converts StAX events to JSON (see: http://blog.bdoughan.com/2011/04/jaxb-and-json-via-jettison.html)
  2. By leveraging a JAXB impl that offers a JSON-binding (see: http://blog.bdoughan.com/2011/08/json-binding-with-eclipselink-moxy.html)
  3. Leveraging a JSON-binding tool that offers support for some JAXB metadata (i.e Jackson).

Since your model doesn't seem to be reacting as expected to the annotations I'm guessing you are using scenario 3. Below I will demonstrate the solution as if you were using scenario 2.

DataWrapper

import javax.xml.bind.annotation.*;

@XmlRootElement
@XmlAccessorType(XmlAccessType.FIELD)
public class DataWrapper { 

    private String a;
    private Data data;

}

Data

import javax.xml.bind.annotation.*;

@XmlAccessorType(XmlAccessType.FIELD)
@XmlSeeAlso({DataSubA.class, DataSubB.class})
public class Data {

}

DataSubA

import javax.xml.bind.annotation.XmlType;

@XmlType(name="1")
public class DataSubA extends Data {

    private String b;

}

DataSubB

import javax.xml.bind.annotation.XmlType;

@XmlType(name="2")
public class DataSubB extends Data {

    private String c;
    private String d;

}

jaxb.properties

To specify MOXy as your JAXB provider you need to include a file called jaxb.properties in the same package as your domain model with the following entry (see: http://blog.bdoughan.com/2011/05/specifying-eclipselink-moxy-as-your.html):

javax.xml.bind.context.factory=org.eclipse.persistence.jaxb.JAXBContextFactory

Demo

import java.util.*;
import javax.xml.bind.*;
import javax.xml.transform.stream.StreamSource;
import org.eclipse.persistence.jaxb.JAXBContextProperties;

public class Demo {

    public static void main(String[] args) throws Exception {
        Map<String, Object> properties = new HashMap<String, Object>();
        properties.put(JAXBContextProperties.MEDIA_TYPE, "application/json");
        properties.put(JAXBContextProperties.JSON_INCLUDE_ROOT, false);
        JAXBContext jc = JAXBContext.newInstance(new Class[] {DataWrapper.class}, properties);

        Unmarshaller unmarshaller = jc.createUnmarshaller();
        StreamSource json = new StreamSource("src/forum16429717/input.json");
        DataWrapper dataWrapper = unmarshaller.unmarshal(json, DataWrapper.class).getValue();

        Marshaller marshaller = jc.createMarshaller();
        marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
        marshaller.marshal(dataWrapper, System.out);
    }

}

input.json/Output

MOXy can read in the numeric value 2 as the inheritance indicator, but currently it will always write it out as "2". I have opened the following enhancement request to address this issue: http://bugs.eclipse.org/407528.

{
   "a" : "abc",
   "data" : {
      "type" : "2",
      "c" : "cde",
      "d" : "def"
   }
}

For More Information

The following link will help you use MOXy in a JAX-RS implementation.

  • http://blog.bdoughan.com/2012/05/moxy-as-your-jax-rs-json-provider.html
like image 159
bdoughan Avatar answered Nov 09 '22 08:11

bdoughan