Michael-O
Michael-O

Reputation: 18405

Rename element names for MOXy JSON marshalling

I use a common JAXB model for JAX-WS (Metro) and JAX-RS (Jersey). I have the following request snippet:

<xs:element name="CreatedProjFolders">
    <xs:complexType>
        <xs:sequence>
            <xs:element name="CreatedProjFolder" type="tns:CreatedProjFolder" minOccurs="0"
                maxOccurs="unbounded" />
        </xs:sequence>
        <xs:attribute name="parentItemId" type="tns:itemId" use="required" />
    </xs:complexType>
</xs:element>

<xs:complexType name="CreateProjFolder">
    <xs:attribute name="itemId" type="tns:itemId" use="required" />
    ...
</xs:complexType>

XJC generated this class:

@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
    "createProjFolders"
})
@XmlRootElement(name = "CreateProjFolders")
public class CreateProjFolders {

    @XmlElement(name = "CreateProjFolder", required = true)
    @NotNull
    @Size(min = 1)
    @Valid
    // Name has been pluralized with JAXB bindings file
    protected List<CreateProjFolder> createProjFolders;
    @XmlAttribute(name = "parentItemId", required = true)
    @NotNull
    @Size(max = 128)
    protected String parentItemId;

    ...
}

The appropriate JSON POST should look like:

{"parentItemId":"P5J00142301", "createProjFolders":[
  {"itemId":"bogus"}
]}

but actually must look like:

{"parentItemId":"P5J00142301", "CreateProjFolder":[
  {"itemId":"bogus"}
]}

How can rename the property name for JSON only resembling the one in Java (protected List<CreateProjFolder> createProjFolders)?

Upvotes: 3

Views: 2325

Answers (2)

Michael-O
Michael-O

Reputation: 18405

After reading Blaise's post and the blog, it took me two days to come up with a working solution. First of all, the current status of MOXyJsonProviderand ConfigurableMoxyJsonProvider make it a pain-in-the-ass exprience to make it work and have never been designed for that.

My first test was to make a clean room implementation which is completely decoupled from Jersey and runs in a main method.

Here is the main method:

public static void main(String[] args) throws JAXBException {

Map<String, Object> props = new HashMap<>();

InputStream importMoxyBinding = MOXyTest.class
        .getResourceAsStream("/json-binding.xml");

List<InputStream> moxyBindings = new ArrayList<>();
moxyBindings.add(importMoxyBinding);

props.put(JAXBContextProperties.OXM_METADATA_SOURCE, moxyBindings);
props.put(JAXBContextProperties.JSON_INCLUDE_ROOT, false);
props.put(JAXBContextProperties.MEDIA_TYPE, MediaType.APPLICATION_JSON);

JAXBContext jc = JAXBContext.newInstance("my.package",
    CreateProjFolders.class.getClassLoader(), props);

Unmarshaller um = jc.createUnmarshaller();

InputStream json = MOXyTest.class
    .getResourceAsStream("/CreateProjFolders.json");
Source source = new StreamSource(json);

JAXBElement<CreateProjFolders> create = um.unmarshal(source, CreateProjFolders.class);
CreateProjFolders folders = create.getValue();

System.out.printf("Used JAXBContext: %s%n", jc);
System.out.printf("Unmarshalled structure: %s%n", folders);

Marshaller m = jc.createMarshaller();
m.setProperty(MarshallerProperties.INDENT_STRING, "    ");
m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true);
System.out.print("Marshalled structure: ");
m.marshal(folders, System.out);

}

Here the json-binding.xml:

<?xml version="1.0" encoding="UTF-8"?>
<xml-bindings xmlns="http://www.eclipse.org/eclipselink/xsds/persistence/oxm"
    package-name="my.package"
    xml-mapping-metadata-complete="false">
    <xml-schema namespace="urn:namespace"
        element-form-default="QUALIFIED" />

    <java-types>
        <java-type name="CreateProjFolders">
            <xml-root-element />
            <java-attributes>
                <xml-element java-attribute="projFolders" name="createProjFolders" />
            </java-attributes>
        </java-type>
        <java-type name="CreateProjFolder">
            <java-attributes>
                <xml-element java-attribute="access" name="access" />
            </java-attributes>
        </java-type>
        <java-type name="Access">
            <java-attributes>
                <xml-element java-attribute="productionSites" name="productionSites" />
            </java-attributes>
        </java-type>
    </java-types>

</xml-bindings>

and a sample input file:

{"parentItemId":"some-id",
    "createProjFolders":[
    {"objectNameEn":"bogus", "externalProjectId":"123456",
        "access":{"productionSites":["StackOverflow"], "user":"michael-o"}}
    ]
}

Unmarshalling and marshalling work flawlessly. Now, how to make it work with Jersey? You can't because you cannot pass JAXBContext properties.

You need to copy MOXy's MOXyJsonProvider and the entire source of Jersey Media MOXy except for XML stuff into a new Maven project because of the AutoDiscoverable feature. This package will replace the original dependency.

Apply the following patches. Patches aren't perfect and can be imporoved because some code is duplicate, thus redundant but that can be done in a ticket later.

Now confiure that in your Application.class:

InputStream importMoxyBinding = MyApplication.class
    .getResourceAsStream("/json-binding.xml");

List<InputStream> moxyBindings = new ArrayList<>();
moxyBindings.add(importMoxyBinding);

final MoxyJsonConfig jsonConfig = new MoxyJsonConfig();
jsonConfig.setOxmMedatadataSource(moxyBindings);
ContextResolver<MoxyJsonConfig> jsonConfigResolver = jsonConfig.resolver();
register(jsonConfigResolver);

Now try it. After several calls on different models you will see JAXBExceptions with 'premature end of file'. You will ask you why?! The reason is that the MOXyJsonProvider creates and caches JAXBContexts per domain class and not per package which means that your input stream is read several times but has already been closed after the first read. You are lost. You need to reset the stream but cannot change the inner guts of MOXy. Here is a simple solution for that:

public class ResetOnCloseInputStream extends BufferedInputStream {

    public ResetOnCloseInputStream(InputStream is) {
        super(is);
        super.mark(Integer.MAX_VALUE);
    }

    @Override
    public void close() throws IOException {
        super.reset();
    }

}

and swap your Application.class for

moxyBindings.add(new ResetOnCloseInputStream(importMoxyBinding));

After you have felt the pain in the ass, enjoy the magic!

Final words:

  • I did not accept Blaise's answer (upvote given) because it was only a fraction of a solution but lead me into the right direction.
  • Both classes make it quite hard to solve a very simple problem, more suprisingly that I am appearantly the only one who wants to pass OXM_METADATA_SOURCE. Seriously?

Upvotes: 1

bdoughan
bdoughan

Reputation: 149017

When MOXy is used as your JSON-binding provider the JSON keys will be the same as what is specfieid in the @XmlElement annotations. For example when you have:

@XmlElement(name = "CreateProjFolder", required = true)
protected List<CreateProjFolder> createProjFolders;

You will get:

{"parentItemId":"P5J00142301", "CreateProjFolder":[
  {"itemId":"bogus"}
]}

If you want different names in JSON than in XML you can leverage MOXy's external mapping metadata to override what has been specified in the annotations:

Upvotes: 2

Related Questions