Spring REST with both JSON and XML
Asked Answered
S

4

10

I want to provide one comprehensive REST API with support for both JSON and XML.

The domain model is of complex type and we note that to produce friendly JSON and XML on the same model using MappingJacksonHttpMessageConverter and JaxbMarshaller respectively tends to give either readable XML or readable JSON 1).

What's the best way to proceed?

1) Due to how objects such as maps, root tags and relations are modelled differently in json than in xml, the objects to serialize needs to be designed differently to get both tidy json and tidy xml. Utilities such as jaxb annotations only goes that far.


I can think of a few candidates

1) Create both a json and xml controller/model

public class Controller { 
   public Foo foo() { 
       return new Foo(); 
   } 
}

public class XmlController extends Controller {
   @Override
   public XmlFoo foo() { 
       return new new XmlFoo(super.foo()); 
   } 
}

public class JsonController extends Controller {
   @Override
   public JsonFoo foo() { 
       return new JsonFoo(super.foo()); 
   } 
}

Given a model object Foo create a JsonFoo and XmlFoo

2) Write a custom message converter

I tried this and it turned out to be a bit complicated since the view must know how to resolve e.g., a Foo to a JsonFoo to be able to serialize it into a readable format.

3) Let each model object serialize itself, e.g.,

public class Foo {
    public String serialize(Serializer s) {
       return s.serialize(this);
    }
}

Based on some arbitration parameter let the controller inject the correct serializer

new Foo(new FooJsonSerializer());
new Foo(new FooXmlSerializer());
Scriptwriter answered 20/8, 2013 at 12:25 Comment(5)
Can you explain what you mean by needing different designs?Wein
JAXB for example can be used to map from/to both XML and JSON with the same annotated class. No need for different design.Declaratory
@chrylis, it's far from trivial to constuct an elaborate java structure which can serialize to both pretty json and pretty xml.Apostles
@JohanSjöberg Ah, I didn't get from your question that you were asking about nested/hierarchical data structures. It seems to me that when you hit the point where you're needing to coordinate the design of the serialization and the POJO, you've hit the point where a custom serializer for the object makes sense.Wein
Why is everyone using view resolvers? They are intended for humans. Message converters are made exactly for M2M communication.Lacroix
P
6

I'm doing this in a current project without using a ContentNegotiatingViewResolver. For one method in my controller:

@RequestMapping(value = "/test", method = RequestMethod.GET)
@ResponseBody
public HttpEntity<BasicResponse> getBasicResponse() {
    return new HttpEntity<BasicResponse>(new BasicResponse());
}

I can receive the following output based on the Accept request header.

Accept: application/xml (requires JAXB2 on the classpath)

<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<basicResponse>
    <errors>
        <message>test1</message>
        <message>test2</message>
    </errors>
</basicResponse>

Accept: application/json (requires Jackson on the classpath)

{
    "errors" : ["test1", "test2"]
}

My response object is simple and uses normal annotations:

package org.mypackage.response;

import java.util.ArrayList;
import java.util.List;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlElementWrapper;
import javax.xml.bind.annotation.XmlRootElement;

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

    @XmlElementWrapper(name = "errors")
    @XmlElement(name = "message")
    private List<String> errors = new ArrayList<String>();

    public BasicResponse() {
        this.errors.add("test1");
        this.errors.add("test2");
    }

    public List<String> getErrors() {
        return errors;
    }

}

The SpringSource spring-mvc-showcase project is also a helpful resource. I think they separate the conversions for different methods, but I am definitely doing this for one method.

I can't quite tell by your question...but if you're looking to serialize the output more than that, @chrylis is correct in that a custom serializer would be your next move. But everything I've ran into (which can get pretty complex, with nested objects in my response) converts perfectly to valid XML or JSON.

Pippas answered 20/8, 2013 at 16:57 Comment(1)
Thanks for your answer, but I wasn't clear enough in my question. I too get valid xml & json, but a pretty xml api becomes an ugly json api and the other way around. I think @chrylis was spot on.Apostles
P
1

You should use the ContentNegotiatingViewResolver.

There is an issue in that a collection of POJOs are not mapped correctly with some XML marshallers. XStream has solutions for this (Moxy too?).

Here's a place to start:

http://blog.springsource.org/2013/06/03/content-negotiation-using-views/

Basically, you use a MappingJacksonView and a similar one for XML, which is a "fake" view that uses Jackson (or an XML marshaller) to marshall your POJO(s) to the correct format.

The server will send back the correct type based on one of:

  • the HTTP Accept header
  • a "filetype extension", such as ".json"
  • a querystring parameter, such as "format=json"
Parlor answered 20/8, 2013 at 16:41 Comment(0)
E
0

As far as omitting fields, you cans use annotations @JsonIgnore(for Jackson) and/or @XStreamOmitField(for XStream).

Did you try this:

@RequestMapping(value = "/{id}", 
method = RequestMethod.GET,
headers ={"Accept=application/json,application/xml"},
produces={"application/json", "application/xml"})
Ehrenberg answered 26/8, 2014 at 20:41 Comment(0)
H
0

Alternatively, You could configure in Spring

@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
    configurer
        .favorParameter(true)
        .defaultContentType(MediaType.APPLICATION_JSON) 
        .mediaType("json", MediaType.APPLICATION_JSON)
        .mediaType("xml", MediaType.APPLICATION_XML);
}

@GetMapping(value = "/health")
public Health health() {
    return Health.builder().name("Application").status("UP").build();
}

http://localhost:18050/api/ping/health?format=xml

http://localhost:18050/api/ping/health?format=json

Heliotaxis answered 15/2 at 1:13 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.