Jackson JSON date format serialization based on condition - java

I am using Mule 3.5.2 and I have a REST service that send and receives JSON messages. The service is for both Norway and Sweden. All dates are send as strings, but Sweden and Norway have different formats. I know by the URL which country calls our service. I am using custom date serializers and deserializers.
I can kind of cheat when receiving JSON message, the formats are different enough that in my custom deserializer I can try one format. If that fails I just try the other. However: How do I serialize in the right format?
It doesn't seem to exist any way to send a parameter to the serializer that this particular message goes to Norway so use this date format...and the next goes to Sweden use another format etc.
Code that I have, that may help:
#GET
#Path("{country:se|no}/{id}")
public Response webservice(#PathParam("country") String country,
#PathParam("id") String id) {
country = country.toUpperCase();
WebServiceResponse response = doWebServiceStuff(id, country)
return Response.ok(reponse).build();
}
Response has a .language() method, but that seems just to affect the headers.
#JsonAutoDetect
public class WebServiceResponse {
#JsonSerialize(using = JsonDateSerializer.class)
#JsonDeserialize(using = JsonDateDeserializer.class)
private Date date;
public void setDate(Date d) { this.date = d; }
public Date getDate() { return this.date; }
}
Serializer today. I would like it to adapt whether it is going to a Norwegian user or Swedish user.
public class JsonDateSerializer extends JsonSerializer<Date> {
private static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
#Override
public void serialize(Date date, JsonGenerator gen, SerializerProvider provider) throws IOException, JsonProcessingException {
String formattedDate = dateFormat.format(date);
gen.writeString(formattedDate);
provider.getConfig().getDateFormat()
}
}
Deserializer. It have about the same problem but I could surround it with try/catch...if the Swedish date format is not valid, try to parse with the Norwegian number instead and throw RuntimeException if it still is a problem.
public class JsonDateDeserializer extends JsonDeserializer<Date> {
#Override
public Date deserialize(JsonParser parser, DeserializationContext deserializationContext)
throws IOException, JsonProcessingException {
String dateText = parser.getText();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
try {
return dateFormat.parse(dateText);
} catch (ParseException e) {
// TODO Auto-generated catch block
throw new RuntimeException("Can't parse date " + dateText, e);
}
}
}
Btw...I am using codehaus version of Jackson as that seems to work with Mule. I tried FasterXML-version but that didn't use my custom serializers nor the new fancy annotation based formatters (so you don't need custom serializers). Version 1.9.11 to be exact.
Again: the question is, how can I control the date format based on conditions from for instance the URL (more exactly from the outside) for each message. I know in webservice-method (the first code block) which country I am talking with but not in the serializer...
Outcome
The solution I got provided below was really a solution that would fix my issue, but I do beleive it is not possible to get it working in Mule 3.5.2 EE. However, if using Mule 3.6.0 or 3.7.0 (that seems to be the latest version now) this will probably be the solution for you, as well as others that might use other frameworks.
Not mentioned in the comments, but I did try commenting out "String country = uriInfo.getPathParameters().getFirst("country");" and hardcoded country to "no" and I did get Norwegian date format. When recompiling it with "se" I did get Swedish format, so the solution really works even though I could not get it to work.
Update2
I did have a discussion with Mule Support. In 3.5.x of Mule and older, jersey-json and jackson-jaxrs are shipped and it is a bit random (and depends on different environments) which it loads. One can remove jersey-json from $MULE_HOME/lib/opt. 3.6.x and later will only ships which jackson-jaxrs.
As I sit on a system with many flows that works, I have not the time to test if removing jersey-json doesn't break anything (as removing the file will affect all flows and not just this one). Basicly 3.6.x and later will have the better control over Jersey (choosing Providers etc.), and will make it possible to get this working.

"...how can I control the date format based on conditions from for instance the URL (more exactly from the outside) for each message"
Though a little bit more work, one way is to create different ObjectMappers configured differently for each type of request. To determine which one will be used we can make the decision inside a ContextResolver. We could inject a UriInfo into the resolver, to get the value of the #PathParam("country"). Then make the decision from that, which mapper will be used. For example
#Provider
public class ObjectMapperContextResolver implements ContextResolver<ObjectMapper> {
private final ObjectMapper sweMapper;
private final ObjectMapper norMapper;
private final ObjectMapper defaultMapper;
#Context
private UriInfo uriInfo;
public ObjectMapperContextResolver() {
defaultMapper = new ObjectMapper();
sweMapper = new ObjectMapper();
SimpleModule sweModule = new SimpleModule("SweModule", new Version(1,0,0,null));
sweModule.addDeserializer(Date.class, new JsonDateDeserializer(sweFormat));
sweModule.addSerializer(Date.class, new JsonDateSerializer(sweFormat));
sweMapper.registerModule(sweModule);
norMapper = new ObjectMapper();
SimpleModule norModule = new SimpleModule("NorModule", new Version(1,0,0,null));
norModule.addDeserializer(Date.class, new JsonDateDeserializer(norFormat));
norModule.addSerializer(Date.class, new JsonDateSerializer(norFormat));
norMapper.registerModule(norModule);
}
#Override
public ObjectMapper getContext(Class<?> type) {
String country = uriInfo.getPathParameters().getFirst("country");
if (country == null) {
return defaultMapper;
}
switch (country) {
case "se": return sweMapper;
case "no": return norMapper;
default: return defaultMapper;
}
}
}
The reason we are using three mapper is for one, they are expensive to create. Secondly, configuring them is not thread-safe. And since the ContextResolver will be a singleton, only one of the mappers will be used for the application. So we just create three for different cases.
If you go this route, you should also remember to remove the serialization annotations from the field.
UPDATE
So with Jersey 2.6, it seems there is a problem with the above solution. It just fails on startup. The solution I was able to find was to not use this dependency
<dependency>
<groupId>com.sun.jersey</groupId>
<artifactId>jersey-json</artifactId>
<version>${jersey-version}</version>
</dependency>
Seem loading of some part of this module causes it to fail. Instead just use the Pure Jackson dependency (which the above actually pulls in and uses itself).
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-jaxrs</artifactId>
<version>1.9.13</version>
</dependency>
Note: jersey-json:1.6 uses 1.7.1 of the above dependency. I just switched to use the latest 1.x version. So you may or may not want to switch it back.
Get rid of any you might have for the old artifact, i.e
<init-param>
<param-name>com.sun.jersey.api.json.POJOMappingFeature</param-name>
<param-value>true</param-value>
</init-param>
And add the Jackson package as a package to scan
<init-param>
<param-name>com.sun.jersey.config.property.packages</param-name>
<param-value>
com.your.packages,
org.codehaus.jackson.jaxrs
</param-value>
</init-param>
Or if you are using some Mule specific configuration, just register these
org.codehaus.jackson.jaxrs.JacksonJaxbJsonProvider
org.codehaus.jackson.jaxrs.JacksonMappingExceptionMapper
org.codehaus.jackson.jaxrs.JacksonParseExceptionMapper

Related

Deserializing badly formatted dates as null in Jackson

I have a Java web application which uses Jackson 2.x to deserialize JSON requests. I'm currently interfacing with an external application which is sending JSON data with improperly formatted dates in one property of one of the classes. This is causing exceptions with that client's calls (as well it should). However, for business reasons we need a temporary workaround to accept these requests.
Until that client fixes the data it sends to my application (which may be a while), I want to treat any bad dates in that property as null. I do not want to change the actual class itself, as it is a public API which exposed to other clients, and I don't want this temporary workaround included in that class.
Is there an easy way to configure Jackson to treat invalid dates as null? I'm seeing similar functionality in DeserializationFeature, but nothing specifically for this.
A solution that would require you overriding the setter of the original class in a subclass:
public class InvalidDateDeserializer extends JsonDeserializer<Date>
{
#Override
public Date deserialize(JsonParser jsonParser,
DeserializationContext deserializationcontext) throws IOException, JsonProcessingException {
SimpleDateFormat sdf = new SimpleDateFormat("dd-MM-yyyy'T'HH:mm:ss");
String date = jsonParser.getText();
try {
return sdf.parse(date);
} catch (ParseException e) {
return null;
}
}
}
Afterwards, you would just need to annotate the overriden setter in your class with
#JsonDeserialize(using = InvalidDateDeserializer.class)
So the new DTO class would look like this:
public MyModel extends TheirModel {
#Override
#JsonDeserialize(using = InvalidDateDeserializer.class)
public void setProblematicDate() {
super.setProblematicDate();
}
}
I understand this is no magic flag solution, but it should work in your case without changes to the original DTO class.

Setter for LocalDateTime (Java 8 API) gets called twice

I've implemented a class for a Java Web App I'm working on. The class has a LocalDateTime property 'created'. However, when I try to set that property (once), its setter is somehow called twice in succession - first setting the value I want, then setting it to null on a second call that should not even happen.
I've traced through the following method and everything looks well up to the third line.
public static ICEDocument mapDocumentFromSOLR(SolrDocument document) {
ICEDocument result = new ICEDocument();
Date uploaded = (Date) document.getFieldValue("CREATED");
LocalDateTime uploadDate = LocalDateUtils.convertUtcDateToLocalDateTime(uploaded); // custom class
result.setCreated(uploadDate); // **faulty line**
}
Here's the class, shortened for clarity:
import java.time.LocalDateTime;
import org.springframework.data.annotation.Transient;
[...]
#JsonIgnoreProperties(ignoreUnknown=true)
public class ICEDocument implements java.io.Serializable {
[...]
#Transient
private LocalDateTime created;
[...]
#JsonDeserialize(using=LocalDateTimeJsonDeserializer.class)
public void setCreated(LocalDateTime created) {
System.out.println("Setting creation date " + created); // added for debugging
this.created = created;
}
}
Steps I've taken trying to resolve this
Removing the #Transient. The data is filled in via Hibernate (ver5.1), and I originally annotated the property since the field itself is not in the corresponding database table. I thought that might be the problem (see Object Serialization and Java Transient Variables), but removing it didn't change anything.
Changing the third line. I switched it with what was basically inside the static LocalDateUtils method. This didn't resolve the issue.
LocalDateTime uploadDate = uploaded.toInstant().atZone(ZoneId.of("UTC")).toLocalDateTime();
Removing the JSON Deserializer. I don't think the JsonDeserializer is at fault since it isn't supposed to (and doesn't accd. to Debug) do anything at this point, but I'll add it here for completeness sake. Could be I'm just grasping at straws at this point.
public class LocalDateTimeJsonDeserializer extends JsonDeserializer<LocalDateTime> {
private static final String DATE_TIME = "yyyy-MM-dd' 'HH:mm:ss";
#Override
public LocalDateTime deserialize(JsonParser parser, DeserializationContext context)
throws IOException, JsonProcessingException {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATE_TIME);
LocalDateTime deserializedDate = LocalDateTime.parse(parser.getText(), formatter);
return deserializedDate;
}
}
Thank you for reading to the end of my rather long post.
After debugging the code I found a line further down that set the property to null. So it was in fact a second call to the setter and a lot of bad luck, I suppose.
But it might help to know that there wasn't anything wrong with the other factors, so I"ll just leave this here. Thanks again.

Custom object deserializes fine in Jax-RS but if it is used in another object it doesn't work

Deserializing works fine if I just pass my custom object through
#POST
public Response saveCustomObject(CustomObject data)
{
// Prints correct value
System.out.println(data);
}
However, if it is a property on another object, it just gets the default value of my custom object
#POST
public Response saveCustomObjectWrapper(CustomObjectWrapper data)
{
// Prints incorrect value
System.out.println(data.getCustomObject());
}
My provider is registered and looks like this:
public CustomObject readFrom(Class<CustomObject> type, Type type1, Annotation[] antns, MediaType mt, MultivaluedMap<String, String> mm, InputStream in) throws IOException, WebApplicationException
{
try {
return new CustomObject(IOUtils.toString(in));
} catch (Exception ex) {
throw new ProcessingException("Error deserializing a CustomObject.", ex);
}
}
The problem is that the reader for all other objects doesn't do lookup/delegation while unmarshalling. What I mean by that, can be seen in this answer, where one reader looks up another reader based on the type. Assuming the format is JSON, whether you're using MOXy (the default with Glassfish) or Jackson, the result is the same. The reader is smart enough to handle the the JSON by itself, so doesn't need to lookup any other readers.
One solution would be to create another reader for the wrapper class, and do lookup/delegation, as seen in the link above. If you have a lot of these situations, you may can extend the default reader, and override its unmarshalling method, but I would completely advise against this, unless you really know what you're doing.
Another solution, depending on the serializer you're using, is to write JsonDeserializer (for Jackson) or XmlAdapter (for MOXy or Jackson). For Jackson an example would be something like (you can see a better example here)
public class CustomObjectDeserializer extends JsonDeserializer<CustomObject> {
#Override
public CustomObject deserialize(JsonParser jp, DeserializationContext dc)
throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
return new CustomObject("Hello World");
}
}
#JsonDeserialize(using = CustomObjectDeserializer.class)
public class CustomObject {
public String message;
public String getMessage() { return message; }
public void setMessage(String message) { this.message = message; }
public CustomObject(String message) { this.message = message; }
public CustomObject(){}
}
In which case, there is no need for a custom reader at all. This will handle CustomObjects and objects that have CustomObject as a member. One problem with this is I'm not sure how or if you can get the InputStream. You just need to use the Jackson APIs to parse the JSON.
If you want to use Jackson instead of the default MOXy for glassfish, you can just add the Jackson dependency
<dependency>
<groupId>org.glassfish.jersey.media</groupId>
<artifactId>jersey-media-json-jackson</artifactId>
<version>2.13</version>
</dependency>
Then register the JacksonFeature, or simply disable MOXy, as mentioned here. If you want to continue using MOXy, I don't know if there is such thing as a class level adapter, so you will still need the reader as well as create a XmlAdapter for class members. It's a bit of a hassle, but that's why I recommend Jackson, for many other reasons, besides this particular use case. You can see an example of an adapter here
Now a lot of this answer is based on the assumption you are using JSON format, as you haven't specified the media type you are using. If it some other format, then I think maybe your only solution is to create another customer reader for the wrapper.

Jackson not naming fields how I want [duplicate]

This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
Different names of JSON property during serialization and deserialization
I am using Jackson on my site to create an options string to be used with a charting tool that expects JSON. So for example, I have a
public class Chart {
Integer zIndex = 3;
public Integer getZIndex() {
return zIndex;
}
}
so then I use Jackson's objectMapper on my chart and the output is {"zindex":3} where my issue is that the charting tool will not accept "zindex" but insists on the camel cased "zIndex".
What can I do to get this to be named properly in the output?
I've tried #JsonProperty("zIndex") but this generates two copies in the output, zindex and zIndex, which is confusing and ugly. Also, I am using lombok to generate my getters, if that makes a difference.
I tried:
public class FieldNamingStrategy extends PropertyNamingStrategy {
#Override
public String nameForField(MapperConfig<?> config, AnnotatedField field, String defaultName) {
return field.getName();
}
}
and then
objectMapper.setPropertyNamingStrategy()
but this didn't work.
My configuration looks like
String json = null;
StringWriter stringWriter = new StringWriter();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonSerialize.Inclusion.NON_NULL);
//TODO: figure this out
objectMapper.setPropertyNamingStrategy(new FieldNamingStrategy());
try {
final JsonGenerator jsonGenerator = objectMapper.getJsonFactory().createJsonGenerator(stringWriter);
jsonGenerator.useDefaultPrettyPrinter();
objectMapper.writeValue(jsonGenerator, object);
json = stringWriter.toString();
Make sure you use a modern version of Jackson: 1.9 improved handling of properties, so that annotation would work even when added to just one of pieces.
Or if you can not do that, just add #JsonProperty annotation to BOTH getter and field.
Your main problem is really that name itself is "non-compliant", meaning that pieces might not match.

JAX-RS Jackson Json Provider Date Format Issue

WRT to the following question:
Jersey + Jackson JSON date format serialization - how to change the format or use custom JacksonJsonProvider.
I wish to know
Is Jackson specifying that the json date format should be normalised to a unix time integer?
Follow-up questions ...
Has there been a change in its stance anytime recently?
Shouldn't a date format be normalised to the same format provided by jaxb xml output?
why/why not?
any effort put into resolving this issue?
has RestEasy provided a json provider mitigation that would output json date in a generally recognisable date format?
Sorry people for yelling out loud - I found the answers here
http://wiki.fasterxml.com/JacksonFAQDateHandling,
here
http://wiki.fasterxml.com/JacksonFAQ#Serializing_Dates,
here
http://wiki.fasterxml.com/JacksonHowToCustomSerializers
here
http://jackson.codehaus.org/1.1.2/javadoc/org/codehaus/jackson/map/util/StdDateFormat.html
Using the #JsonSerialize(using= ... ) way:
public class JsonStdDateSerializer
extends JsonSerializer<Date> {
private static final DateFormat iso8601Format =
StdDateFormat.getBlueprintISO8601Format();
#Override
public void serialize(
Date date, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException {
// clone because DateFormat is not thread-safe
DateFormat myformat = (DateFormat) iso8601Format.clone();
String formattedDate = myformat.format(date);
jgen.writeString(formattedDate);
}
}
This is also controlled by a feature on the ObjectMapper (at least in 1.9.11, and possibly earlier):
ObjectMapper om = new ObjectMapper();
om.configure(SerializationConfig.Feature.WRITE_DATES_AS_TIMESTAMPS, false);
om.writer().writeValue(System.out, objectWithDateProperty);
I don't see how to declaratively do the equivalent on the object definition itself.

Categories