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.
Related
I'm trying to deserialize a JSON structure with Jackson and I'm working with a DTO that looks like this:
public class RootLevelDTO {
private List<ComplexEntry> complexEntries;
// ... other fields, not relevant
}
Now, the ComplexEntry can have sub-types, those have properties of enum types etc. A lot can go wrong here if the other side of the communication updates their API and e.g. adds another sub type or adds an enum literal.
What I would like to do is to tell Jackson:
if you encounter any databinding error during deserialization of the complexEntries field...
... do not throw an exception, but instead ignore this entry and continue with the next.
What I tried so far is to use a delegating deserializer for ComplexEntry:
public class ComplexEntryDeserializer extends StdDeserializer<ComplexEntry> {
private StdDeserializer<ComplexEntry> delegate;
public ComplexEntryDeserializer(StdDeserializer<ComplexEntry> delegate){
this.delegate = delegate;
}
public ComplexEntry deserialize(JsonParser p, DeserializationContext ctxt){
try {
return this.delegate.deserialize(p, ctxt);
}catch(Exception e){
// the list entry failed to deserialize, but we have to return *something* here
return null;
}
}
// ... other mandatory methods, not relevant here
}
This solution has the problem that it will introduce null values to the complexEntries list, which I then have to explicitly get rid of with a Converter.
Is there a more elegant solution to this problem?
After a lot of tinkering I've ended up with the following solution. It doesn't require any additional jackson modules or other magic, only a single (specific) deserializer.
DTO:
public class RootLevelDTO {
// use a custom deserializer for the list
#JsonDeserialize(using = ListOfComplexEntryDeserializer.class)
private List<ComplexEntry> complexEntries;
}
Deserializer:
public class ListOfComplexEntryDeserializer extends JsonDeserializer<List<ComplexEntry>> {
#Override
public List<ComplexEntry> deserialize(JsonParser p, DeserializationContext ctxt) {
List<ComplexEntry> resultList = new ArrayList<>();
while(p.nextToken() != JsonToken.END_ARRAY){
try {
// delegate the deserialization of the individual list entries to the standard deserializers
resultList.add(ctxt.readValue(p, ComplexEntry.class))
}catch(Exception e){
// log that the entry wasn't deserialized properly
System.out.println("ComplexEntry could not be read and will be ignored.");
}
}
return resultList;
}
}
Big disclaimer: While the code above works, it's not something you should go for by design. I'm really with my back to the wall here and have no other choice (due to external factors beyond my control), and for that case it works.
I have lots of beans and all use LocalDate and LocalDateTime. The DateTextField in Wicket and all other widgets (like the DatePicker) only work on java.util.Date. Is there any way to inject a converter into Wicket 7 so that it uses LocalDate or LocalDateTime?
The beans look like this:
public class SomeBean {
Long id = null;
LocalDate since = null;
// plus getters and setters
}
A Wicket form currently uses a CompoundPropertyModel
CompoundPropertyModel<SomeBean> model = new CompundPropertyModel<>( bean );
You can wrap your LocalDate and etc. models in a IModel<java.util.Date>, e.g.
public static class LocalDateModel implements IModel<java.util.Date> {
private IModel<LocalDate> localDateModel;
public LocalDateModel(IModel<LocalDate> localDateModel){
this.localDateModel = localDateModel;
}
#Override
public Date getObject() {
return convertLocalDateToUtilDateSomehow(localDateModel.getObject());
}
#Override
public void setObject(Date object) {
localDateModel.setObject(convertUtilDateToLocalDateSomehow(object));
}
#Override
public void detach() {
localDateModel.detach();
}
}
If you then feed models like this into the form components you want to use it should work just fine.
If you want your CompoundPropertyModel to automatically provide such wrapping models, you need to extend it and override it's CompoundPropertyModel#wrapOnInheritance(Component component) method to infer that a wrapping model is needed. Something like
#Override
public <C> IWrapModel<C> wrapOnInheritance(Component component)
{
IWrapModel<C> actualModel = super.wrapOnInheritance(component);
if (actualModel.getObject() instanceOf LocalDate) {
return new LocalDateModelButAlsoWrapping(actualModel);
} else {
return actualModel;
}
}
Where LocalDateModelButAlsoWrapping is unsurprisingly just an extension of LocalDateModel example above but which also implements IWrapModel<T>.
If you use this extension instead of your regular CompoundPropertyModel it would detect when fields are LocalDate and provide models to components (like your DateTextField) that are wrapped to look like java.util.Date models.
The code snippet I gave you is rather dirty though (you should probably not get the model object to infer its type) as I have only provided it to illustrate the general mechanism, so I suggest you devise your own way to infer the type of object expected (e.g. you can check if the Component argument is a DateTextField), but this is the general direction of the solution that I can imagine.
You can register your own converters:
https://ci.apache.org/projects/wicket/guide/7.x/guide/forms2.html#forms2_3
#Override
protected IConverterLocator newConverterLocator() {
ConverterLocator defaultLocator = new ConverterLocator();
defaultLocator.set(Pattern.class, new RegExpPatternConverter());
return defaultLocator;
}
Related: https://issues.apache.org/jira/browse/WICKET-6200
You can simply backport the converter classes from Wicket 8. You'll find these attached to this commit: https://issues.apache.org/jira/browse/WICKET-6200 (AbstractJavaTimeConverter and whatever subclasses you need for LocalDate, LocalDateTime, LocalTime, etc.)
Of course, that will not help with a DateTextField, because that has the Date type parameter hardcoded. For such, you can either create your own subclasses using the above converters, or use regular Label and TextField, with converters registered globally, as shown below:
#Override
protected IConverterLocator newConverterLocator() {
ConverterLocator converterLocator = new ConverterLocator();
converterLocator.set(LocalDateTime.class, new LocalDateTimeConverter());
converterLocator.set(LocalDate.class, new LocalDateConverter());
converterLocator.set(LocalTime.class, new LocalDateConverter());
return converterLocator;
}
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.
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
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.