I'm fetching addresses from an external API. This is the class representing the addresses:
#JsonInclude(JsonInclude.Include.NON_NULL)
public class Address implements Serializable {
private static final long serialVersionUID = -7134571546367230214L;
private String street;
private int houseNumber;
private String district;
private String city;
private String state;
private String zipCode;
}
However, when the given address doesn't have a houseNumber, the API will return a string such as "NO NUMBER" on the houseNumber field, causing Jackson to throw a deserialization error, since it was expecting an integer number and got a string.
How can I tell Jackson to convert houseNumber to 0 when it finds a string value?
You could try with a custom deserializer on the field:
#JsonDeserialize(using = HouseNoDeserializer.class)
private int houseNumber;
The deserializer could look like this:
class HouseNoDeserializer extends JsonDeserializer<Integer> {
#Override
public Integer deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
//read the value as a string since you don't know whether it is a number or a string
String v = p.readValueAs(String.class);
try {
//try to parse the string to an integer, if not return 0 as required (it is a non-numeric string)
return Integer.parseInt(v);
} catch(NumberFormatException nfe) {
return 0;
}
}
}
However, I'd change houseNumber to take String anyways because right now you can't support numbers such as "1/c", "123a", etc. which are common at least in some countries.
You could then do without a custom deserializer and simply add some logic to the setter or apply it after parsing the json, i.e. replace "NO NUMBER" with another value as needed.
You can provide custom com.fasterxml.jackson.databind.deser.DeserializationProblemHandler and handle all these kind of business values generally for all POJO classes:
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.deser.DeserializationProblemHandler;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.IOException;
public class HandleErrorsApp {
private final static JsonMapper JSON_MAPPER = JsonMapper.builder()
.enable(SerializationFeature.INDENT_OUTPUT)
.addModule(new JavaTimeModule())
.addHandler(new ProjectDeserializationProblemHandler())
.build();
public static void main(String[] args) throws Exception {
var json = "{\"houseNumber\":\"NO_ADDRESS\"}";
var address = JSON_MAPPER.readValue(json, Address.class);
System.out.println(address);
}
}
class ProjectDeserializationProblemHandler extends DeserializationProblemHandler {
#Override
public Object handleWeirdStringValue(DeserializationContext ctxt, Class<?> targetType, String valueToConvert, String failureMsg) throws IOException {
if (targetType == int.class && valueToConvert.equals("NO_ADDRESS")) {
return 0;
}
return super.handleWeirdStringValue(ctxt, targetType, valueToConvert, failureMsg);
}
}
#Data
#NoArgsConstructor
#AllArgsConstructor
class Address {
private int houseNumber;
}
Or you can provide custom com.fasterxml.jackson.databind.util.StdConverter implementation which has a little bit simpler interface than JsonDeserializer:
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.fasterxml.jackson.databind.util.StdConverter;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.math.NumberUtils;
public class HandleErrorsApp {
private final static JsonMapper JSON_MAPPER = JsonMapper.builder()
.enable(SerializationFeature.INDENT_OUTPUT)
.addModule(new JavaTimeModule())
.build();
public static void main(String[] args) throws Exception {
var json = "{\"houseNumber\":\"NO_ADDRESS\"}";
var address = JSON_MAPPER.readValue(json, Address.class);
System.out.println(address);
}
}
class StringIntConverter extends StdConverter<String, Integer> {
#Override
public Integer convert(String value) {
return NumberUtils.toInt(value, 0);
}
}
#Data
#NoArgsConstructor
#AllArgsConstructor
class Address {
#JsonDeserialize(converter = StringIntConverter.class)
private int houseNumber;
}
In both cases program prints:
Address(houseNumber=0)
Related
I need to create a json from an Object. The Object contains a List member variable of type Name. Name is a class which contains an enum NameType. below are the class:
public final class Person { private final List<Name> names; }
public class Name {
private final String first;
private final String last;
private final String middle;
private final NameType type;
}
public enum NameType {
X,
Y,
Z
}
The json that is produced for class Person should not include Name for which NameType is Y and Z. Below is the simple way in which I try to generate the json:
Map<String, Object> personMap = mapper.readValue(person!=null ? mapper.writeValueAsString(person) : "{}", typeReference);
I need to remove the key "names" from personMap. I have searched a few ways to remove it before serialization, but that hasn't worked for me. I followed this tutorial: https://www.baeldung.com/jackson-ignore-properties-on-serialization
and tried to remove it using type and using filter, but because this is an enum so didn't work for me. So, I have 2 questions:
Is there a way to not include the Name property as part of serialization based on NameType.
If there is any way to remove after serialization is done.
Below is the structure that is generated for the map.
Thanks in Advance!!
Disclaimer: I never used Jackson before, this was my first contact. I was just looking for a little puzzle to solve while drinking my morning tea, which also enables me to learn something. So I am not sure if there are better or more elegant ways of doing this.
In order to present an MCVE which everyone can easily compile and execute, here are my more complete versions (including getters) of your example classes:
package de.scrum_master.stackoverflow.q71358052;
public enum NameType {
REAL,
ARTIST,
ONLINE
}
package de.scrum_master.stackoverflow.q71358052;
public class Name {
private final String first;
private final String last;
private final String middle;
private final NameType type;
public Name(String first, String last, String middle, NameType type) {
this.first = first;
this.last = last;
this.middle = middle;
this.type = type;
}
public String getFirst() {
return first;
}
public String getLast() {
return last;
}
public String getMiddle() {
return middle;
}
public NameType getType() {
return type;
}
}
package de.scrum_master.stackoverflow.q71358052;
import java.util.List;
public final class Person {
private final List<Name> names;
public Person(List<Name> names) {
this.names = names;
}
public List<Name> getNames() {
return names;
}
}
As described for a similar case in this tutorial, we can use a custom JsonSerializer<Name> in combination with a BeanSerializerModifier, registering them on the ObjectMapper in a SimpleModule:
package de.scrum_master.stackoverflow.q71358052;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import java.io.IOException;
public class NameSerializer extends JsonSerializer<Name> {
private final JsonSerializer<Object> defaultSerializer;
public NameSerializer(JsonSerializer<Object> serializer) {
defaultSerializer = serializer;
}
#Override
public void serialize(Name value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value.getType() != NameType.REAL)
return;
defaultSerializer.serialize(value, gen, serializers);
}
#Override
public boolean isEmpty(SerializerProvider provider, Name value) {
return value == null || value.getType() != NameType.REAL;
}
}
package de.scrum_master.stackoverflow.q71358052;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
import java.util.List;
public class JacksonDemo {
public static void main(String[] args) throws JsonProcessingException {
Person person = new Person(List.of(
new Name("Edward", "Robinson", "G.", NameType.ARTIST),
new Name("Emanuel", "Goldenberg", null, NameType.REAL),
new Name("Эмануэль", "Голденберг", null, NameType.REAL),
new Name("Eddie", "The Gangster", null, NameType.ONLINE)
));
ObjectMapper objectMapper = getObjectMapper();
String personJson = objectMapper.writeValueAsString(person);
System.out.println(personJson.contains("ARTIST")); // false
System.out.println(personJson.contains("ONLINE")); // false
System.out.println(personJson.contains("Goldenberg")); // true
System.out.println(personJson.contains("Голденберг")); // true
System.out.println(personJson);
}
private static ObjectMapper getObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
objectMapper.registerModule(
new SimpleModule() {
#Override
public void setupModule(SetupContext context) {
super.setupModule(context);
context.addBeanSerializerModifier(
new BeanSerializerModifier() {
#Override
public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {
if (Name.class.isAssignableFrom(beanDesc.getBeanClass()))
return new NameSerializer((JsonSerializer<Object>) serializer);
return serializer;
}
}
);
}
}
);
return objectMapper;
}
}
The console log should be:
false
false
true
true
{"names":[{"first":"Emanuel","last":"Goldenberg","type":"REAL"},{"first":"Эмануэль","last":"Голденберг","type":"REAL"}]}
Oh, by the way and just in case not everybody knows who Edward G. Robinson was...
I'm coding a functionality of handling callbacks from Amazon Simple Email Service via SNS HTTP requests. I would like to parse message provided by Amazon to local object structure. Problem is that SNS is wrapping JSON message into String and it could not be parsed by Jackson. I'm getting an error:
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `xxx.email.domain.aws.ses.Notification` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('{"notificationType":"Delivery","mail":{"timestamp":"2019-10-02T14:43:14.570Z" ... next values of the message ... }}')
Entire message from SNS looks like this one:
{
"Type" : "Notification",
"MessageId" : "4944xxxx-711d-57d4-91b8-8215cxxxxx",
"TopicArn" : "arn:aws:sns:eu-west-1:...",
"Message" : "{\"notificationType\":\"Delivery\",\"mail\":{\"timestamp\":\"2019-10-02T14:43:14.570Z\", ... next values of the message ... },\"delivery\":{\"timestamp\":\"2019-10-02T14:43:16.030Z\", ... next values of the message ... }}",
"Timestamp" : "2019-10-02T14:43:16.062Z",
"SignatureVersion" : "1",
"Signature" : "signature base64",
"SigningCertURL" : "cert url",
"UnsubscribeURL" : "unsubscribe url"
}
My actual local structure looks like this:
#Data
#JsonNaming(PropertyNamingStrategy.UpperCamelCaseStrategy.class)
public class MessageWrapper {
private String type;
private String messageId;
private String topicArn;
private Notification message;
private Date timestamp;
private String signatureVersion;
private String signature;
private String signingCertURL;
private String unsubscribeURL;
}
#Data
public class Notification {
private String notificationType;
private Mail mail;
}
#Data
public class Mail {
private String messageId;
private String source;
private String sourceArn;
private String sourceIp;
private String sendingAccountId;
private String[] destination;
}
I'm looking for some way to tell Jackson that Message should be extracted from a String and treated as a normal JSON.
Edit
deserialization
private MessageWrapper deserializeMessage(String message) throws IOException {
return new ObjectMapper().readValue(message, MessageWrapper.class);
}
I think to solve this you'll need a custom deserializer for Notification field in MessageWrapper class as well as one for the Mail field in the Notification class the like the following:
public class NotificationDeserializer extends JsonDeserializer<Notification> {
#Override
public Notification deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
String text = p.getText();
return new ObjectMapper().readValue(text, Notification.class);
}
}
public class MailDeserializer extends JsonDeserializer<Mail> {
#Override
public Mail deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
String text = p.getText();
return new ObjectMapper().readValue(text, Mail.class);
}
}
With some annotations on your classes like the following:
#Data
#JsonNaming(PropertyNamingStrategy.UpperCamelCaseStrategy.class)
public class MessageWrapper {
private String type;
private String messageId;
private String topicArn;
#JsonDeserialize(using = NotificationDeserializer.class)
private Notification message;
private Date timestamp;
private String signatureVersion;
private String signature;
private String signingCertURL;
private String unsubscribeURL;
}
#Data
public class Notification {
private String notificationType;
#JsonDeserialize(using = MailDeserializer.class)
private Mail mail;
}
#Data
public class Mail {
private String messageId;
private String source;
private String sourceArn;
private String sourceIp;
private String sendingAccountId;
private String[] destination;
}
EDIT 1
The MailDeserializer isn't actually needed. The NotificationDeserializer alone takes care of the issue.
EDIT 2
Using a new ObjectMapper in the custom deserializer is a must.
message property is of type Notification and Jackson expects JSON Object not string value. In that case you can create custom deserialiser or implement general solution with some kind of loop back implementation. If given payload is not a JSON Object read it as a String and invoke deserialisation again with this String.
To avoid StackOverflowError you need to use another instance of ObjectMapper or use BeanDeserializerModifier to keep BeanDeserializer instance and use it where JSON Object is encountered. Simple example could look like below:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonToken;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.PropertyNamingStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import com.fasterxml.jackson.databind.deser.BeanDeserializer;
import com.fasterxml.jackson.databind.deser.BeanDeserializerBase;
import com.fasterxml.jackson.databind.deser.BeanDeserializerModifier;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.node.TextNode;
import lombok.Data;
import lombok.ToString;
import java.io.File;
import java.io.IOException;
import java.util.Collections;
import java.util.Date;
import java.util.Objects;
import java.util.Set;
public class JsonApp {
public static void main(String[] args) throws Exception {
File jsonFile = new File("./resource/test.json").getAbsoluteFile();
SimpleModule loopBackModule = new SimpleModule();
loopBackModule.setDeserializerModifier(new LoopBackBeanDeserializerModifier(Collections.singleton(Notification.class)));
ObjectMapper mapper = new ObjectMapper();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(loopBackModule);
MessageWrapper wrapper = mapper.readValue(jsonFile, MessageWrapper.class);
System.out.println(wrapper.getMessage());
}
}
class LoopBackBeanDeserializerModifier extends BeanDeserializerModifier {
private final Set<Class> allowedClasses;
LoopBackBeanDeserializerModifier(Set<Class> allowedClasses) {
this.allowedClasses = Objects.requireNonNull(allowedClasses);
}
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
if (allowedClasses.contains(beanDesc.getBeanClass())) {
return new LoopBackBeanDeserializer<>((BeanDeserializerBase) deserializer);
}
return deserializer;
}
}
class LoopBackBeanDeserializer<T> extends BeanDeserializer {
private final BeanDeserializerBase baseDeserializer;
protected LoopBackBeanDeserializer(BeanDeserializerBase src) {
super(src);
this.baseDeserializer = src;
}
#Override
public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
// if first token is VALUE_STRING we should read it as String and
// run deserialization process again based on this String.
if (p.currentToken() == JsonToken.VALUE_STRING) {
return (T) ((ObjectMapper) p.getCodec()).readValue(p.getText(), _valueClass);
}
// vanilla bean deserialization
return (T) baseDeserializer.deserialize(p, ctxt);
}
}
POJO model is the same. You just need to list classes for which you expect some problems and loop-back mechanism will work for them.
So the problem is the next:
I have POJO like:
#Data
#Accessors(chain = true)
#JsonInclude(Include.NON_NULL)
public class TestPOJO {
private Long id;
private String name;
private JsonNode jsonNode;
Also I have json like
{
"id":1
"name": "foo"
"jsonNode":null
}
When I try deserialize the last one by the
ObjectMapper objectMapper = new ObjectMapper();
TestPOJO testPojo = objectMapper.readValue(<json String>, TestPOJO.class);
I get testPojo object where jsonNode field is NullNode, but I need in testPojo == null
How I can fix it?
Add a class that extends JsonDeserializer<JsonNode> and that returns null if parser.getText() is null:
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import java.io.IOException;
public class JsonNodeDeserializer extends JsonDeserializer<JsonNode> {
#Override
public JsonNode deserialize(JsonParser parser, DeserializationContext context)
throws IOException {
String value = parser.getText();
if(value == null) {
return null;
}
return (JsonNode) context.findRootValueDeserializer(context.constructType(JsonNode.class)).deserialize(parser, context);
}
}
Then annotate the jsonNode attribute with #JsonDeserialize(using = JsonNodeDeserializer.class) to tell Jackson to use your custom deserializer:
#Data
#Accessors(chain = true)
#JsonInclude(Include.NON_NULL)
public class TestPOJO {
private Long id;
private String name;
#JsonDeserialize(using = JsonNodeDeserializer.class)
private JsonNode jsonNode;
// getters and setters
}
I am consuming a "RESTful" service (via RestTemplate) that produces JSON as follows:
{
"id": "abcd1234",
"name": "test",
"connections": {
"default": "http://foo.com/api/",
"dev": "http://dev.foo.com/api/v2"
},
"settings": {
"foo": "{\n \"fooId\": 1, \"token\": \"abc\"}",
"bar": "{\"barId\": 2, \"accountId\": \"d7cj3\"}"
}
}
Note the settings.foo and settings.bar values, which cause issues on deserialization. I would like to be able to deserialize into objects (e.g., settings.getFoo().getFooId(), settings.getFoo().getToken()).
I was able to solve this specifically for an instance of Foo with a custom deserializer:
public class FooDeserializer extends JsonDeserializer<Foo> {
#Override
public Foo deserialize(JsonParser jp, DeserializationContext ctx) throws IOException {
JsonNode node = jp.getCodec().readTree(jp);
String text = node.toString();
String trimmed = text.substring(1, text.length() - 1);
trimmed = trimmed.replace("\\", "");
trimmed = trimmed.replace("\n", "");
ObjectMapper mapper = new ObjectMapper();
JsonNode obj = mapper.readTree(trimmed);
Foo result = mapper.convertValue(obj, Foo.class);
return result;
}
}
#Data
#JsonIgnoreProperties(ignoreUnknown = true)
public class Settings {
#JsonDeserialize(using = FooDeserializer.class)
private Foo foo;
private Bar bar;
}
However, now if I want to deserialize settings.bar, I need to implement another custom deserializer. So I implemented a generic deserializer as follows:
public class QuotedObjectDeserializer<T> extends JsonDeserializer<T> implements ContextualDeserializer {
private Class<?> targetType;
private ObjectMapper mapper;
public QuotedObjectDeserializer(Class<?> targetType, ObjectMapper mapper) {
this.targetType = targetType;
this.mapper = mapper;
}
#Override
public JsonDeserializer<T> createContextual(DeserializationContext context, BeanProperty property) {
this.targetType = property.getType().containedType(1).getRawClass();
return new QuotedObjectDeserializer<T>(this.targetType, this.mapper);
}
#Override
public T deserialize(JsonParser jp, DeserializationContext context) throws IOException {
JsonNode node = jp.getCodec().readTree(jp);
String text = node.toString();
String trimmed = text.substring(1, text.length() - 1);
trimmed = trimmed.replace("\\", "");
trimmed = trimmed.replace("\n", "");
JsonNode obj = this.mapper.readTree(trimmed);
return this.mapper.convertValue(obj, this.mapper.getTypeFactory().constructType(this.targetType));
}
}
Now I'm not sure how to actually use the deserializer, as annotating Settings.Foo with #JsonDeserialize(using = QuotedObjectDeserializer.class) obviously does not work.
Is there a way to annotate properties to use a generic custom deserializer? Or, perhaps more likely, is there a way to configure the default deserializers to handle the stringy objects returned in my example JSON?
Edit: The problem here is specifically deserializing settings.foo and settings.bar as objects. The JSON representation has these objects wrapped in quotes (and polluted with escape sequences), so they are deserialized as Strings.
Sorry about the length of the code here. There are plenty of shortcuts here (no encapsulation; added e to defaulte to avoid keyword etc.) but the intent is there
Model class:
package com.odwyer.rian.test;
import java.io.IOException;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class Model {
public String id;
public String name;
public Connections connections;
public Settings settings;
public static class Connections {
public String defaulte;
public String dev;
#Override
public String toString() {
return ReflectionToStringBuilder.toString(this);
}
}
public static class Foo {
public Foo () {}
#JsonCreator
public static Foo create(String str) throws JsonParseException, JsonMappingException, IOException {
return (new ObjectMapper()).readValue(str, Foo.class);
}
public Integer fooId;
public String token;
#Override
public String toString() {
return ReflectionToStringBuilder.toString(this);
}
}
public static class Bar {
public Bar() {}
#JsonCreator
public static Bar create(String str) throws JsonParseException, JsonMappingException, IOException {
return (new ObjectMapper()).readValue(str, Bar.class);
}
public Integer barId;
public String accountId;
#Override
public String toString() {
return ReflectionToStringBuilder.toString(this);
}
}
public static class Settings {
public Foo foo;
public Bar bar;
#Override
public String toString() {
return ReflectionToStringBuilder.toString(this);
}
}
#Override
public String toString() {
return ReflectionToStringBuilder.toString(this);
}
}
The caller:
package com.odwyer.rian.test;
import java.io.File;
import java.io.IOException;
import java.util.Scanner;
import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class TestClass {
private static ObjectMapper objectMapper = new ObjectMapper();
public static void main(String[] args) throws JsonParseException, JsonMappingException, IOException {
Scanner file = new Scanner(new File("test.json"));
String jsonStr = file.useDelimiter("\\Z").next();
Model model = objectMapper.readValue(jsonStr, Model.class);
System.out.println(model.toString());
}
}
The result (too much hassle to format out but it is all there!):
com.odwyer.rian.test.Model#190083e[id=abcd1234,name=test,connections=com.odwyer.rian.test.Model$Connections#170d1f3f[defaulte=http://foo.com/api/,dev=http://dev.foo.com/api/v2],settings=com.odwyer.rian.test.Model$Settings#5e7e6ceb[foo=com.odwyer.rian.test.Model$Foo#3e20e8c4[fooId=1,token=abc],bar=com.odwyer.rian.test.Model$Bar#6291bbb9[barId=2,accountId=d7cj3]]]
The key, courtesy of Ted and his post (https://stackoverflow.com/a/8369322/2960707) is the #JsonCreator annotation
I've got a web service which manages Parada objects. What I want to achieve seems straightforward: return lists of these objects:
List<Parada> list
This list is returned using a Service class which uses another DAO class, just commenting it out.
Besides, my common practise is that every web method return a Response using ResponseBuilder, as in here:
return Response.ok(obj, MediaType.APPLICATION_JSON).build();
This is an example of one of my web methods:
#GET
#Consumes(value = MediaType.TEXT_PLAIN)
#Produces(MediaType.APPLICATION_JSON)
#Path("{idParadaGtfs}")
public Response getParadasPorIdGtfs(
#PathParam(value = "idParadaGtfs") Integer pCodigoParadaEnGtfs
){
try{
getServiceIfNecessary();
List<Parada> paradas = service.getParadas(pCodigoParadaEnGtfs);
return Response.ok(paradas, MediaType.APPLICATION_JSON).build();
}catch(HibernateException e){
String msg = "Error HibernateException: " + e.getMessage();
LogHelper.logError(logger, msg, true);
e.printStackTrace();
return Response.serverError().tag(msg).build();
}catch(Exception e){
String msg = "Error Exception: " + e.getMessage();
LogHelper.logError(logger, msg, true);
e.printStackTrace();
return Response.serverError().tag(msg).build();
}
}
Unfortunately, I'm not receiving any object and I get the following error everytime I execute the web method described above:
nov 26, 2015 2:20:16 PM org.glassfish.jersey.message.internal.WriterInterceptorExecutor$TerminalWriterInterceptor aroundWriteTo
GRAVE: MessageBodyWriter not found for media type=application/json, type=class java.util.ArrayList, genericType=java.util.List<model.Parada>.
What do I have to implement to let my web methods build Responses using Lists?
Thank you!
EDIT:
I've been able to make it work by making some changes and additions, which I'll describe now.
First of all, I've added a Parada container class, ParadaContainer:
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import javax.xml.bind.annotation.XmlRootElement;
import com.ingartek.ws.paradasasociadasws.model.Parada;
#XmlRootElement
public class ParadaContainer implements Serializable {
private static final long serialVersionUID = 6535386309072039406L;
private List<Parada> paradas;
public ParadaContainer(ArrayList<Parada> pParadas) {
this.setParadas(pParadas);
}
public List<Parada> getParadas() {
return paradas;
}
public void setParadas(List<Parada> paradas) {
this.paradas = paradas;
}
#Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("ParadaContainer [");
if (paradas != null) {
builder.append("paradas=");
for(Parada p : paradas){
builder.append(p.toString());
}
}
builder.append("]");
return builder.toString();
}
}
Now, I'm not returning a List of Parada objects, instead I return a single ParadaContainer object:
ParadaContainer paradas = new ParadaContainer(new ArrayList<Parada>(service.getParadas()));
return Response
.ok(paradas)
.type(MediaType.APPLICATION_JSON)
.build();
I don't know whether they are mandatory or not, but I've created another class (MyObjectMapperProvider)...
import javax.ws.rs.ext.ContextResolver;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
public class MyObjectMapperProvider implements ContextResolver<ObjectMapper> {
final ObjectMapper defaultObjectMapper;
public MyObjectMapperProvider() {
defaultObjectMapper = createDefaultMapper();
}
#Override
public ObjectMapper getContext(Class<?> type) {
return defaultObjectMapper;
}
private static ObjectMapper createDefaultMapper() {
final ObjectMapper result = new ObjectMapper();
result.configure(SerializationFeature.INDENT_OUTPUT, true);
return result;
}
}
...and edited my Application class and added some lines (see as of *Jackson * comment until Clases de Servicios comment):
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import javax.ws.rs.core.Application;
import org.glassfish.jersey.jackson.JacksonFeature;
import com.ingartek.ws.paradasasociadasws.ws.ParadasWS;
public class App extends Application {
private final Set<Class<?>> classes;
public App() {
HashSet<Class<?>> c = new HashSet<Class<?>>();
// Filtro CORS:
c.add(CORSFilter.class);
// Jackson
c.add(MyObjectMapperProvider.class);
c.add(JacksonFeature.class);
// Clases de Servicios:
c.add(ParadasWS.class);
classes = Collections.unmodifiableSet(c);
}
#Override
public Set<Class<?>> getClasses() {
return classes;
}
}
Afterwards, I've edited my class model by adding some annotations to them (#XmlRootElement and #JsonProperty; removed irrelevant getters, setters, hashCode, equals and toString methods):
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.xml.bind.annotation.XmlRootElement;
import com.fasterxml.jackson.annotation.JsonProperty;
#XmlRootElement(name = "grupo")
#Entity
#Table(name = "grupos_cercania_exacta")
public class Grupo implements Serializable {
#Transient
private static final long serialVersionUID = -5679016396196675191L;
#JsonProperty("id")
#Id
#Column(name = "id_grupo")
private Integer id;
...
}
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.xml.bind.annotation.XmlRootElement;
import com.fasterxml.jackson.annotation.JsonProperty;
#XmlRootElement(name = "operador")
#Entity
#Table(name = "operadores_asociados")
public class Operador implements Serializable {
#Transient
private static final long serialVersionUID = -7557099187432476588L;
/*
Atributos
*/
#JsonProperty("codigo")
#Id
#Column(name = "codigo_operador", insertable = false, updatable = false)
private Integer codigo;
#JsonProperty("nombre")
#Column(name = "descripcion_corta", insertable = false, updatable = false)
private String nombre;
#JsonProperty("descripcion")
#Column(name = "descripcion_larga", insertable = false, updatable = false)
private String descripcion;
#JsonProperty("web")
#Column(name = "direccion_web", insertable = false, updatable = false)
private String web;
#JsonProperty("telefono")
#Column(name = "telefono", insertable = false, updatable = false)
private String telefono;
...
}
import java.io.Serializable;
import java.util.UUID;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.xml.bind.annotation.XmlRootElement;
import com.fasterxml.jackson.annotation.JsonProperty;
#XmlRootElement(name = "parada")
#Entity
#Table(name = "paradas_asociadas")
public class Parada implements Serializable {
#Transient
private static final long serialVersionUID = -3594254497389126197L;
#JsonProperty("id")
#Id
#Column(name = "id")
private UUID id;
#JsonProperty("codigoMunicipio")
#Column(name = "codigo_municipio")
private Integer codigoMunicipio;
#JsonProperty("nombre")
#Column(name = "nombre")
private String nombre;
#JsonProperty("descripcion")
#Column(name = "descripcion")
private String descripcion;
#JsonProperty("idGtfs")
#Column(name = "id_gtfs")
private Integer idGtfs;
#JsonProperty("idWs")
#Column(name = "id_ws")
private Integer idWs;
#JsonProperty("latitud")
#Column(name = "latitud")
private Double latitud;
#JsonProperty("longitud")
#Column(name = "longitud")
private Double longitud;
#JsonProperty("utmX")
#Column(name = "utm_x")
private Double utmX;
#JsonProperty("utmY")
#Column(name = "utm_y")
private Double utmY;
#JsonProperty("grupo")
#ManyToOne
#JoinColumn(name = "grupo_cercania_exacta_id")
private Grupo grupo;
#JsonProperty("operador")
#ManyToOne
#JoinColumn(name = "operador")
private Operador operador;
...
}
I've to admit that I've had some problems just after these changes. Sharp people could've realised that there is a missing attribute regarding the previous Parada class: the lack of Point attribute. This attribute was causing me some problems, this is, the absence of a Serializer and a Serializer was preventing me from creating a successful JSON. So I googled it out and found three options:
Remove the Point item. This was my ultimate choice, as Point was superfluous due to the existence of Latitude and Longitude elements and because it just could bother or confuse the final user.
Creating a custom Serializer and Deserializer. Fortunately I found the following link, which describes the process of creating them. The following is described in here:
Add these annotations to our coordinates field:
#JsonSerialize(using = PointToJsonSerializer.class)
#JsonDeserialize(using = JsonToPointDeserializer.class)
Create such serializer:
import java.io.IOException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import com.vividsolutions.jts.geom.Point;
public class PointToJsonSerializer extends JsonSerializer<Point> {
#Override
public void serialize(Point value, JsonGenerator jgen,
SerializerProvider provider) throws IOException,
JsonProcessingException {
String jsonValue = "null";
try
{
if(value != null) {
double lat = value.getY();
double lon = value.getX();
jsonValue = String.format("POINT (%s %s)", lat, lon);
}
}
catch(Exception e) {}
jgen.writeString(jsonValue);
}
}
Create such deserializer:
import java.io.IOException;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.vividsolutions.jts.geom.Coordinate;
import com.vividsolutions.jts.geom.GeometryFactory;
import com.vividsolutions.jts.geom.Point;
import com.vividsolutions.jts.geom.PrecisionModel;
public class JsonToPointDeserializer extends JsonDeserializer<Point> {
private final static GeometryFactory geometryFactory = new GeometryFactory(new PrecisionModel(), 26910);
#Override
public Point deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
try {
String text = jp.getText();
if(text == null || text.length() <= 0)
return null;
String[] coordinates = text.replaceFirst("POINT ?\\(", "").replaceFirst("\\)", "").split(" ");
double lat = Double.parseDouble(coordinates[0]);
double lon = Double.parseDouble(coordinates[1]);
Point point = geometryFactory.createPoint(new Coordinate(lat, lon));
return point;
}
catch(Exception e){
return null;
}
}
}
The last option is to use Jackson Datatype JTS library, whose github repository lays here.
I lasted some hours so that I could find these solutions, but finally I got them. Hope it helps to someone. Thank you!
Either you don't have a JSON provider (I am guessing you do) or you are using MOXy. Under the latter assumption, with MOXy, it needs to know the type information in order to be able to serialize. When you return Response, you are wrapping the object, which takes away type information (because of type erasure), as opposed to if you were doing
#GET
public List<Parada> get() {}
Here the type information is known. But doing
#GET
public Response get() {
List<Parada> list..
return Response.ok(list)...
}
The type is hidden and erased by the time the entity reaches the serialization phase of the processing.
To get around this, GenericEntity was introduced
Normally type erasure removes generic type information such that a Response instance that contains, e.g., an entity of type List<String> appears to contain a raw List<?> at runtime. When the generic type is required to select a suitable MessageBodyWriter, this class may be used to wrap the entity and capture its generic type.
So you can do
List<Parada> paradas = ...
GenericEntity<List<Parada>> entity = new GenericEntity<List<Parada>>(paradas){};
return Response.ok(entity, ...)...
Second option, is to instead of using MOXy, use Jackson instead. With Jackson, the type info is not needed (in most cases), as the serializer just introspects and the bean bean properties to get the data.
It is not allowed to send a List back. Probably because List has no #XmlRootElement notation. You can create your own container:
#XmlRootElement
public class ParadaContainer implements Serializable {
private List<Parada> list;
public List<Parada> getList() {
return list;
}
public void setList(List<Parada> list) {
this.list = list;
}
}
You part will look like:
try{
getServiceIfNecessary();
List<Parada> paradas = service.getParadas(pCodigoParadaEnGtfs);
ParadaContainer paradaContainer = new ParadaContainer();
paradaContainer.setList(paradas);
return Response.ok(paradaContainer, MediaType.APPLICATION_JSON).build();
}