We are migrating from Jersey 1 to Jersey 2.
Up until now we were using ContextResolver configured like this:
import com.sun.jersey.api.json.JSONConfiguration;
import com.sun.jersey.api.json.JSONConfiguration.MappedBuilder;
#Provider
#Produces("application/json")
public class JSONJAXBContextResolver implements ContextResolver<Class<?>> {
#Override
public JAXBContext getContext(Class<?> objectType) {
MappedBuilder mapped = JSONConfiguration.mapped();
mapped.arrays("Property"); //$NON-NLS-1$
mapped.arrays("option"); //$NON-NLS-1$
JSONConfiguration build = mapped.xml2JsonNs(NamespacesMapper.getNamespacesMap()).build();
return new JSONJAXBContext(build, objectType);
}
}
All is good, the produced json looked like this(root xml element is unwrapped i.e. removed):
{"#id":"as213","code":"ERR12","cause":{"validationMessages":{"validationMessage":{"message":"some message","details":"some details","severity":"ERROR"}}}}
However, with jersey 2 there is no more JSONConfiguration.mapped().
Instead we are looking into jettison way of doing the same thing.
So we now have:
#Provider
#Produces("application/json")
public class JSONJAXBContextResolver implements ContextResolver<Class<?>> {
#Override
public JAXBContext getContext(Class<?> objectType) {
MappedJettisonBuilder mappedJettison = JettisonConfig.mappedJettison();
mappedJettison.serializeAsArray("Property"); //$NON-NLS-1$
mappedJettison.serializeAsArray("option"); //$NON-NLS-1$
JettisonConfig build = mappedJettison.xml2JsonNs(NamespacesMapper.getNamespacesMap()).build();
return new JettisonJaxbContext(build, objectType);
}
However this produces the following:
{"error":{"#id":"as213","code":"ERR12","cause":{"validationMessages":{"validationMessage":{"message":"some message","details":"some details","severity":"ERROR"}}}}}
Notice the "root" element "error". This breaks our JSON representation big time.
I've spent almost 2 days now trying to figure out how to configure Jettison to exclude the xml root element, but to no avail.
I've noticed the following in the JettisonConfig javadoc:
https://jersey.java.net/apidocs/2.1/jersey/org/glassfish/jersey/jettison/JettisonConfig.html#DEFAULT
public static final JettisonConfig DEFAULT
The default JettisonConfig uses JettisonConfig.Notation.MAPPED_JETTISON notation with root unwrapping option set to true.
However even using DEFAULT configuration instead of Mapped does not produce the desired json - the root "error" element is still there.
I've even looked in the Jettison source for configuration property controlling this behavior but could not find anything.
Does anyone know how and if it is possible to make Jettison ignore the root XML element?
Related
I have a Spring Boot application (2.4.1), where an OffsetDateTime field is returned as float value from RestController. Example:
"created_at": 1616080724.531610100
I tried all the suggested solutions in this thread. None of them worked for me.
I also tried to add a very simple end-point that only returns OffsetDateTime:
#GetMapping("/test")
public OffsetDateTime test() {
return OffsetDateTime.now();
}
The result is the same, it's returned as float value.
Then I tried the same end-point in a minimal Spring Boot project and it's returned in ISO format as expected:
"2021-03-18T15:39:14.5295632+01:00"
This all points to some transitive dependency messing up with the default Jackson serializers used by Sprint Boot. But mvn dependency:tree doesn't give me any suspicious dependencies (e.g. no gson marshaller dependency).
I also tried enabling TRACE logging, and I can see that the object written in HttpEntityMethodProcessor has the correctly formatted created_at time:
TRACE org.springframework.web.servlet.mvc.method.annotation.HttpEntityMethodProcessor - Writing [class MyObject {
....
createdAt: 2021-03-18T16:37:34.113316500+01:00
...
But it still ends up as float on the client side (testing on browser and with Postman). What could be the problem here?
After some debugging in Jackson classes, I found out that InstantSerializerBase#serialize method was being called with the default SerializerProvider (DefaultSerializerProviderImpl), which had the SerializationFeature.WRITE_DATES_AS_TIMESTAMPS feature enabled. That resulted in serializing OffsetDateTime values as epoch seconds + nanos.
I was able to fix the problem by adapting our WebMvcConfigurer implementation as follows:
#Configuration
#EnableWebMvc
public class WebConfiguration implements WebMvcConfigurer {
// Some other configuration
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new MappingJackson2HttpMessageConverter(objectMapper()));
}
private ObjectMapper objectMapper() {
return new ObjectMapper()
.disable(WRITE_DATES_AS_TIMESTAMPS)
.registerModule(new JavaTimeModule());
}
}
After this change, OffsetDateTime fields are finally serialized in ISO format; e.g.
"created_at": "2021-03-19T17:05:27.785646+01:00"
Soution with configureMessageConverters is exacty what I needed. I have the same problem and you really helped me. Thanks!
Maybe you should report the solution to Spring
my code
#GetMapping(value = {"/metadata"}, produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseStatus(HttpStatus.OK)
#ResponseBody
public String getMetadata() {
return dppService.getMetadata();
}
the method getMetadata will just return a json string. it just read data from the json file, and it is in another library can not be changed.
But when call this api, i got the follow reponse:
"{\"Namespace\":\"com.xxx\"...
the json string was escaped.
expected:
"{"Namespace":"com.xxx"...
How could i make it return the right json? BTW, our other services also return a json string in the controller, but their response will not be escaped which is so confused for me.
You could do this two ways:
From what I could understand you are having this issues because you might be returning the json as a string from from the service method dppService.getMetadata() by converting it manually to a string. If so , change that and instead return a POJO class from the service method as well as the controller, spring default jackson converter should automatically convert it to a json when the request is served. (I would suggest you go with this approach)
Another approach (the hacky less desirable one) if you still want to keep returning a string then you could configure the StringMessageConverter like below to accept json:
#Override
public void configureMessageConverters(
List<HttpMessageConverter<?>> converters) {
StringHttpMessageConverter stringConverter = new StringHttpMessageConverter(
Charset.forName("UTF-8"));
stringConverter.setSupportedMediaTypes(Arrays.asList( //
MediaType.TEXT_PLAIN, //
MediaType.TEXT_HTML, //
MediaType.APPLICATION_JSON));
converters.add(stringConverter);
}
root cause:
There is a configuration file in the project:
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new MappingJackson2HttpMessageConverter(jacksonBuilder().build()));
converters.stream()
.filter(converter -> converter instanceof MappingJackson2HttpMessageConverter)
.findFirst()
.ifPresent(converter -> ((MappingJackson2HttpMessageConverter) converter).setDefaultCharset(UTF_8));
}
This configuration overrite the defualt jackson behavior. There are two ways to solve this issue:
1.Remove this configuration, then it will be the default behavior
2.Add the StringHttpMessageConverter in this configuration, see Ananthapadmanabhan's option2
So I had a perfectly working Spring app. Most of my controller methods are for ajax calls that return JSON via #ResponseBody with the jackson api and returns my Java POJO to JSON.
I have a need to turn XML to JSON, so I find that Jackson has a tool for that, and I add this to my POM to use the library:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.9.0</version>
</dependency>
So that I may use this:
XmlMapper xmlMapper = new XmlMapper();
JsonNode node = xmlMapper.readTree(sb.toString().getBytes());
But now the #ResponseBody is returning XML and not JSON. I Remove the dependency and the controllers return JSON again.
Any way to get both? I want the xmlMapper, and JSON from the response body.
jackson-dataformat-xml appears to be registering a MappingJackson2HttpMessageConverter with a XmlMapper, along with other HttpMessageConverters that work with XML. If you always intended to return JSON from your controllers, you can change what HttpMessageConverter your app uses by overriding configureMessageConverters
For Spring 5.0 and above,
#Configuration
public class HttpResponseConfig implements WebMvcConfigurer {
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.removeIf(converter -> supportsXml(converter) || hasXmlMapper(converter));
}
private boolean supportsXml(HttpMessageConverter<?> converter) {
return converter.getSupportedMediaTypes().stream()
.map(MimeType::getSubtype)
.anyMatch(subType -> subType.equalsIgnoreCase("xml"));
}
private boolean hasXmlMapper(HttpMessageConverter<?> converter) {
return converter instanceof MappingJackson2HttpMessageConverter
&& ((MappingJackson2HttpMessageConverter)converter).getObjectMapper().getClass().equals(XmlMapper.class);
}
}
For older versions of Spring, replace implements WebMvcConfigurer with extends WebMvcConfigurerAdapter
Add Accept: application/json to HTTP request header.
Read this for an analysis of how Spring does content negotiation and allows producing either XML or JSON.
The simplest way is to add an extension at the URL: Instead of /path/resource use /path/resource.json
You may also add a format parameter e.g. /path/resource?format=json or pass an appropriate Accept header
In my case, the XmlMapper was actually inserted into the application context as an #Bean. The other solutions here did not work for me. It seems like one of those issues where context matters, so for people coming here from a different context than the other answerers, here's another angle: I had to insert my own ObjectMapper.
#Configuration
public class XmlMapperConfiguration {
#Bean // me, culprit
public XmlMapper xmlMapper() {
return new XmlMapper();
}
#Bean // to make sure the rest of the application still works with JSON
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
}
I'm going to throw in an #Primary on the ObjectMapper one. It seems suspicious that Spring would even choose that one consistently. Since XmlMapper extends ObjectMapper, why would it not take that one, so #Primary won't hurt.
I've defined and registered some custom marshallers for my domain objects. If used alone, just rendering one instance, works fine, but the problem comes when I return a map with an array of those instances. In this moment my custom marshaller is not beign invoked.
This is one of my marshallers:
class BackendCompanyMarshaller implements ObjectMarshaller<JSON> {
#Override
public boolean supports(Object object) {
object instanceof Company
}
#Override
public void marshalObject(Object object, JSON converter)
throws ConverterException {
JSONWriter writer = converter.getWriter()
writer.object()
writer.key('id').value(object.id)
.key('name').value(object.name?.encodeAsHTML()?:'')
.key('description').value(object.description?.encodeAsHTML()?:'')
.key('enterprise').value(object.enterprise?.encodeAsHTML()?:'')
writer.endObject()
}
}
Ans for example this is what I'm returning from my controller:
render text:[achievements:arrayOfAchievements, total:2] as JSON
In previous versions of grails I know there was deep marshallers but I haven't been able to find something similar for grails 3.
I have also tried to implement a custom marshaller for List, but I'm not sure what I should return or write.
I have a JAX-RS WebService with the following method:
#Path("/myrest")
public class MyRestResource {
...
#GET
#Path("/getInteger")
#Produces(APPLICATION_JSON)
public Integer getInteger() {
return 42;
}
When accessed using this snipped:
#Test
public void testGetPrimitiveWrapers() throws IOException {
// this works:
assertEquals(new Integer(42), new ObjectMapper().readValue("42", Integer.class));
// that fails:
assertEquals(new Integer(42), resource().path("/myrest/getInteger").get(Integer.class));
}
I get the following exception:
com.sun.jersey.api.client.ClientResponse getEntity
SEVERE: A message body reader for Java class java.lang.Integer, and Java type class java.lang.Integer, and MIME media type application/json was not found
com.sun.jersey.api.client.ClientResponse getEntity
SEVERE: The registered message body readers compatible with the MIME media type are: application/json
...
The problem is just with returning single primitive values (int/boolean) or their wrapper classes. Returning other POJO classes is not the problemen so I guess all the answers regarding JSONConfiguration.FEATURE_POJO_MAPPING and JAXB annotations do not apply here.
Or which annotation should I use to describe the return type if I don't have access to its
class source?
Using ngrep I can verify that just the String "42" is returned by the webservice. Thats a valid JSON "value" but not a valid JSON "text" according to the spec. So is my problem on the client or the server side?
I tried activating JSONConfiguration natural/badgerfish according to http://tugdualgrall.blogspot.de/2011/09/jax-rs-jersey-and-single-element-arrays.html but with no success (ngrep still shows just "42"). Would that be the right path?
Any ideas are appreciated!
This is a recognized bug in Jackson, which has been touted (incorrectly in my opinion) as a feature. Why do I consider it a bug? Because while serialization works, deserialization definitely does not.
In any case, valid JSON cannot be generated from your current return type, so I would recommend creating a wrapper class:
class Result<T> {
private T data;
// constructors, getters, setters
}
#GET
#Path("/getInteger")
#Produces(APPLICATION_JSON)
public Result<Integer> getInteger() {
return new Result<Integer)(42);
}
Alternatively, you can elect to wrap root values, which will automatically encapsulate your data in a top level JSON object, keyed by the objects simple type name - but note that if this option is used that all generated JSON will be wrapped (not just for primitives):
final ObjectMapper mapper = new ObjectMapper()
.configure(SerializationFeature.WRAP_ROOT_VALUE, true)
.configure(DeserializationFeature.UNWRAP_ROOT_VALUE, true);
final String serializedJson = mapper.writeValueAsString(42);
final Integer deserializedVal = mapper.readValue(serializedJson,
Integer.class);
System.out.println(serializedJson);
System.out.println("Deserialized Value: " + deserializedVal);
Output:
{"Integer":42}
Deserialized Value: 42
See this answer for details on how to retrieve and configure your ObjectMapper instance in a JAX-RS environment.