I'm on a project that require FasterXML and ZonedDateTime. It is mandatory to not use annotation like using=Deserializer.class because we want to keep the same format of date on all the project.
That being said, I come here to ask help because I can't figure out why my Deserializer is instanciate but its method deserialize() is not call ...
Here my configuration :
The serializer :
public class ZonedDateTimeSerializer extends JsonSerializer<ZonedDateTime> {
/**
* The date time formatter to use
*/
DateTimeFormatter dtf;
/**
* Instanciate a new serializer
*
* #param dtf The datetime formatter
*/
public ZonedDateTimeSerializer(DateTimeFormatter dtf) {
System.out.println("ZonedDateTimeSerializer()");
this.dtf = dtf;
}
#Override
public void serialize(ZonedDateTime dateTime, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
System.out.println("ZonedDateTimeSerializer::serialize()");
if (null != dateTime) {
jsonGenerator.writeString(dateTime.format(dtf));
}
}
}
The deserializer :
public class ZonedDateTimeDeserializer extends JsonDeserializer<ZonedDateTime> {
/**
* The date time formatter to use
*/
DateTimeFormatter dtf;
/**
* Instanciate a new deserializer
*
* #param dtf The datetime formatter
*/
public ZonedDateTimeDeserializer(DateTimeFormatter dtf) {
Preconditions.checkNotNull(dtf, "Date time formatter is null");
System.out.println("ZonedDateTimeDeserializer()");
this.dtf = dtf;
}
#Override
public ZonedDateTime deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
System.out.println("ZonedDateTimeDeserializer::deserialize()");
return ZonedDateTime.parse(jsonParser.getText(), dtf);
}
}
This two add to the mapper in a mapper provider :
#Provider
public class JacksonConfigurator implements ContextResolver<ObjectMapper> {
private ObjectMapper mapper = new ObjectMapper();
/**
* Constructor
*/
public JacksonConfigurator() {
System.out.println("new ObjectMapperResolver()");
DateTimeFormatter sdf = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ");
SimpleModule xxxModule = new SimpleModule("XXXX", new Version(2, 0, 0, null))
.addSerializer(ZonedDateTime.class, new ZonedDateTimeSerializer(sdf))
.addDeserializer(ZonedDateTime.class, new ZonedDateTimeDeserializer(sdf));
mapper.registerModule(xxxModule);
}
#Override
public ObjectMapper getContext(Class<?> arg0) {
return mapper;
}
}
And register here in a glassfish jersey ResourceConfig;
#ApplicationPath("api")
public class ApplicationRessourceConfig extends ResourceConfig {
/**
* Define application config
*/
public ApplicationRessourceConfig() {
// File to parse
packages(true, "com.xxx.xxx");
// CDI to Jersey
register(new WebServiceBinder());
// Jackson config
register(new JacksonConfigurator());
// Role
register(RolesAllowedDynamicFeature.class);
}
}
I can't test it in production or development until the tests are ok. So I make the test like :
public class xxxTest extends JerseyTest {
...
#Override
protected Application configure() {
...
ResourceConfig config = new ResourceConfig(...);
config.register(new JacksonConfigurator());
return config;
}
#Test
public void xxx() {
...
response.bufferEntity();
response.readEntity(EntityWithZonedDateTime.class);
...
}
}
When I was using the annotation #JsonDeserializer on each ZonedDateTime property it worked perfectly but now, the Serializer constructor is logged, serialize() is logged too, event the Deserializer constructor is logged, but not the deserialize() method.
Instead I have :
Caused by: com.fasterxml.jackson.databind.JsonMappingException: Can not instantiate value of type [simple type, class java.time.ZonedDateTime] from String value ('2012-06-30T12:30:40.000+0000'); no single-String constructor/factory method
at [Source: org.glassfish.jersey.message.internal.ReaderInterceptorExecutor$UnCloseableInputStream#3af4e0bf; line: 1, column: 46] (through reference chain: com.xxx.xxx.resources.utils.Xxxx["beginDate"])
at com.fasterxml.jackson.databind.JsonMappingException.from(JsonMappingException.java:148)
at com.fasterxml.jackson.databind.DeserializationContext.mappingException(DeserializationContext.java:843)
at com.fasterxml.jackson.databind.deser.ValueInstantiator._createFromStringFallbacks(ValueInstantiator.java:277)
at com.fasterxml.jackson.databind.deser.std.StdValueInstantiator.createFromString(StdValueInstantiator.java:284)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromString(BeanDeserializerBase.java:1150)
at com.fasterxml.jackson.databind.deser.BeanDeserializer._deserializeOther(BeanDeserializer.java:153)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:144)
at com.fasterxml.jackson.databind.deser.SettableBeanProperty.deserialize(SettableBeanProperty.java:523)
at com.fasterxml.jackson.databind.deser.impl.FieldProperty.deserializeAndSet(FieldProperty.java:101)
at com.fasterxml.jackson.databind.deser.impl.BeanPropertyMap.findDeserializeAndSet(BeanPropertyMap.java:285)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:248)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:136)
at com.fasterxml.jackson.databind.ObjectReader._bind(ObjectReader.java:1408)
at com.fasterxml.jackson.databind.ObjectReader.readValue(ObjectReader.java:858)
at com.fasterxml.jackson.jaxrs.base.ProviderBase.readFrom(ProviderBase.java:777)
At 2pm, it will be 48 hours I'm on it. Impossible to know why the mapper doesn't try to desiarilize with the deserializer and it fallback on the string. And guess what ... The fasterxml wiki is down ... Any clue is welcome.
--- EDIT ---
For information, the entity sent in request look like :
#XmlRootElement
public class XxxOutput {
#JsonInclude(JsonInclude.Include.NON_EMPTY)
public static class XxxJson {
/* ---------- Properties ---------- */
public BigInteger id;
public String name;
public String definition;
public ZonedDateTime beginDate;
public ZonedDateTime endDate;
public ZonedDateTime creationDate;
public ZonedDateTime lastUpdate;
public Map<String, BigInteger> relateds;
/* ---------- Constructor ---------- */
public XxxJson() {}
public XxxJson(... all args ...) {
this.id = id;
...
this.relateds = relateds;
}
/* ---------- Useful methods ---------- */
public XxxJson addRelated(final Xxx related) {
...
// add the Xxx.id to the list
...
}
#Override
public String toString() {
return id.toString();
}
}
}
Related
I have implemented a Formatter in a REST Spring Boot web service application to format all LocalDateTime (Java 8) attributes so that they get displayed in a specific format in the web service response.
public class LocalDateTimeFormatter implements Formatter<LocalDateTime> {
#Override
public String print(LocalDateTime temporal, Locale locale) {
DateTimeFormatter formatter = DateTimeFormatter
.ofPattern("yyyy-MM-dd HH:mm:ss");
return formatter.format(temporal);
}
#Override
public LocalDateTime parse(String text, Locale locale)
throws ParseException {
return LocalDateTime.parse(text);
}
}
The formatter has also been registered as shown below so that it works at the application level in order to avoid annotating all LocalDateTime attributes individually
#Configuration
public class ContentFormatter implements WebMvcConfigurer {
#Override
public void addFormatters(FormatterRegistry registry) {
registry.addFormatter(new LocalDateTimeFormatter());
}
}
But no formatting is applied to the response from the web service, accessDate being the LocalDateTime type of attribute
{
"content" : "Hello, World!",
"id" : 0,
"accessDate" : "2020-04-07T19:56:41.48"
}
You probably don't even need to implement your formatted and register it. Just annotate your LocalDateTime property as follows:
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
public LocalDateTime getTime() {
return time;
}
Also, you might need to add the following dependency:
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.6.0</version>
</dependency>
See this question and its answer for details:
Spring Data JPA - ZonedDateTime format for json serialization
You can use StdSerializer
public class JacksonLocalDateSerializer extends StdSerializer<LocalDate> {
private static final long serialVersionUID = -7880057299936771237L;
private static final DateTimeFormatter formatter =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
.withResolverStyle(ResolverStyle.STRICT);
public JacksonLocalDateSerializer() {
this(null);
}
public JacksonLocalDateSerializer(Class<LocalDate> type) {
super(type);
}
#Override
public void serialize(LocalDate value, JsonGenerator jsonGenerator,
SerializerProvider serializerProvider) throws IOException, JsonProcessingException {
jsonGenerator.writeString(formatter.format(value));
}
}
Then add configuration for the serializer for applicable the entire application
#Configuration
public class JacksonConfig {
#Bean
#Primary
public ObjectMapper configureObjectMapper() {
JavaTimeModule javaTimeModule = new JavaTimeModule();
javaTimeModule.addSerializer(LocalDateTime.class, new JacksonLocalDateTimeSerializer());
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(javaTimeModule);
return mapper;
}
}
I am trying to setup my spring boot configuration to use my custom serializer and deserializer.
Deserializer code
public class CustomZonedDateTimeDeserializer extends JsonDeserializer<ZonedDateTime> {
#Override
public ZonedDateTime deserialize(JsonParser jsonParser, DeserializationContext context)
throws IOException, JsonProcessingException {
ObjectCodec oc = jsonParser.getCodec();
TextNode node = (TextNode) oc.readTree(jsonParser);
String dateString = node.textValue();
return ZonedDateTime.parse(dateString, CustomZonedDateTimeSerializer.formatter);
}
#Override
public Class<?> handledType() {
return ZonedDateTime.class;
}
}
Serializer code
public class CustomZonedDateTimeSerializer extends JsonSerializer<ZonedDateTime> {
#Override
public Class<ZonedDateTime> handledType() {
return ZonedDateTime.class;
}
public final static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssZ");
#Override
public void serialize(ZonedDateTime date, JsonGenerator generator, SerializerProvider provider)
throws IOException, JsonProcessingException {
final String dateString = date.format(formatter);
generator.writeString(dateString);
}
}
This my configuration:
#Configuration
public class JacksonConfiguration {
#Autowired
Jackson2ObjectMapperBuilder jacksonBuilder;
#PostConstruct
public void configureJackson() {
final CustomZonedDateTimeSerializer serializer = new CustomZonedDateTimeSerializer();
jacksonBuilder.serializers(serializer);
final CustomZonedDateTimeDeserializer deserializer = new CustomZonedDateTimeDeserializer();
jacksonBuilder.deserializers(deserializer);
}
}
I am creating a REST API so I have this in my controller:
#RequestMapping(value = "date", method = { RequestMethod.GET })
#ApiOperation(value = "", notes = "")
public ResponseWrapper<String> testDATE(
#RequestParam #ApiParam(value = "", required = true) ZonedDateTime date) {
System.out.println(date.toString());
return new ResponseWrapper<String>(date.toString());
}
So far I have tried:
creating a custom module and adding it as a #Bean in the configuration, adding the serializer and deserializer to the jackonBuilder object in configuration and something like the code in this example
Am I missing something or doing something wrong?
Each time I try I get the following error:
"Failed to convert value of type 'java.lang.String' to required type
'java.time.ZonedDateTime'; nested exception is
org.springframework.core.convert.ConversionFailedException: Failed to
convert from type [java.lang.String] to type
[#org.springframework.web.bind.annotation.RequestParam
#io.swagger.annotations.ApiParam java.time.ZonedDateTime] for value
'2018-07-10T00:00:00+0000'; nested exception is
java.lang.IllegalArgumentException: Parse attempt failed for value
[2018-07-10T00:00:00+0000]"
I am sending the following value each time: 2018-07-10T00:00:00+0000 and the value doesn't even reach the deserializer where I have my breakpoint.
Add this bean in your configuration class:
#Bean
public Jackson2ObjectMapperBuilder configureObjectMapper() {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
final CustomZonedDateTimeSerializer serializer = new CustomZonedDateTimeSerializer();
builder.serializers(serializer);
final CustomZonedDateTimeDeserializer deserializer = new CustomZonedDateTimeDeserializer();
builder.deserializers(deserializer);
return builder;
}
Seems like I could not send ZonedDateTime in GET requests. Once I changed to POST, Spring was able to parse the date automatically.
I only added spring.jackson.deserialization.adjust_dates_to_context_time_zone=true so it takes the timezone into consideration.
If someone will want to send it in GET he will have to send a string and parse that instead of defining ZonedDateTime as a parameter.
I have the following class that includes a list of Date which I want to mashall. I have created the JaxBAdapter for the Date class, but it seems it is not called.
I think the problem is the fact that I'm using a list and not just a Date variable.
Could you give me some hint on how should I code the adapter or annotate the class so each element of the list is mashalled using the adapter?
Will it work for JSON serialization as well? I'm planning to use this classes on my REST webservice.
Root.java
#XmlRootElement(name = "root")
#XmlAccessorType(XmlAccessType.FIELD)
public class Root {
#XmlElementWrapper(name="timePeriods")
#XmlElement(name = "timePeriod")
#JsonProperty(value = "timePeriod")
#XmlJavaTypeAdapter(value = JaxBDateThreadSafeAdapter.class, type = Date.class)
private List<Date> timePeriod;
public Root() {
this(new ArrayList<String>(), new ArrayList<Date>(2));
}
public Root(List<Date> timePeriod) {
this.timePeriod = new ArrayList<Date>(timePeriod);
}
}
JaxBAdapter
public class JaxBDateThreadSafeAdapter extends XmlAdapter<String, Date> {
/**
* Thread safe {#link DateFormat}.
*/
private static final ThreadLocal<DateFormat> DATE_FORMAT_TL =
new ThreadLocal<DateFormat>() {
#Override
protected DateFormat initialValue() {
// return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// ISO 8601 format
return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
}
};
#Override
public String marshal(Date v) throws Exception {
return DATE_FORMAT_TL.get().format(v);
}
#Override
public Date unmarshal(String v) throws Exception {
return DATE_FORMAT_TL.get().parse(v);
}
}
The #XmlJavaTypeAdapter works as such with java.util.List as well. But the problem in the adapter class is the date format that is used. JAXB (atleast 2.x onwards) is not strict and dont report such errors and quietly suppresses it.
return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
change to
return new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
I am using Jackson fasterxml for unmarshalling JSON. In my object there are two kinds of properties:Input properties and Calculated properties. In the input JSON, I get only input values.
The calculated values are actually dependent on input values. I have to populate these values before the object gets referred. So I am just checking if there are any hooks provided by Jackson so that I can do my calculations there. For example JAXB provides afterUnmarshal method to customize the unmarshaling behavior:
void afterUnmarshal(Unmarshaller u, Object parent)
But I could not find similar information about customizing Jackson. Are any such framework hooks provided by Jackson to customize the unmarshaling behavior?
I'd rather recommend to keep your model objects immutable by using constructor creators. That is, all the JSON values are passed to a constructor which would initialize the other calculated properties.
Anyway, if you want to customize an object after deserialization (without writing a deserializer for every type) you can modify the deserializer in a way that at the end it calls a special method(s) of a newly constructed instance. Here is an example which would work for all the classes that implements a special interface (one can consider using an annotation to mark the post construct methods).
public class JacksonPostConstruct {
public static interface PostConstructor {
void postConstruct();
}
public static class Bean implements PostConstructor {
private final String field;
#JsonCreator
public Bean(#JsonProperty("field") String field) {
this.field = field;
}
public void postConstruct() {
System.out.println("Post construct: " + toString());
}
#Override
public String toString() {
return "Bean{" +
"field='" + field + '\'' +
'}';
}
}
private static class PostConstructDeserializer extends DelegatingDeserializer {
private final JsonDeserializer<?> deserializer;
public PostConstructDeserializer(JsonDeserializer<?> deserializer) {
super(deserializer);
this.deserializer = deserializer;
}
#Override
protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) {
return deserializer;
}
#Override
public Object deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
Object result = _delegatee.deserialize(jp, ctxt);
if (result instanceof PostConstructor) {
((PostConstructor) result).postConstruct();
}
return result;
}
}
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
BeanDescription beanDesc,
final JsonDeserializer<?> deserializer) {
return new PostConstructDeserializer(deserializer);
}
});
mapper.registerModule(module);
String json = "{\"field\":\"value\"}";
System.out.println(mapper.readValue(json, Bean.class));
}
}
Output:
Post construct: Bean{field='value'}
Bean{field='value'}
Let's assume that your JSON looks like this:
{
"input1" : "Input value",
"input2" : 3
}
And your POJO class looks like this:
class Entity {
private String input1;
private int input2;
private String calculated1;
private long calculated2;
...
}
In this case you can write a custom deserializer for your Entity class:
class EntityJsonDeserializer extends JsonDeserializer<Entity> {
#Override
public Entity deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException,
JsonProcessingException {
InnerEntity innerEntity = jp.readValueAs(InnerEntity.class);
Entity entity = new Entity();
entity.setInput1(innerEntity.input1);
entity.setInput2(innerEntity.input2);
entity.recalculate();
return entity;
}
public static class InnerEntity {
public String input1;
public int input2;
}
}
In above class you can see that Entity has a recalculate method. It could look like this:
public void recalculate() {
calculated1 = input1 + input2;
calculated2 = input1.length() + input2;
}
You can also move this logic to your deserializer class.
Now, you have to inform Jackson that you want to use your custom deserializer:
#JsonDeserialize(using = EntityJsonDeserializer.class)
class Entity {
...
}
The example below shows how to use these classes:
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.readValue(json, Entity.class));
This program prints:
Entity [input1=Input value, input2=3, calculated1=Input value3, calculated2=14]
How can I use different deserializer for different scenraios.
Eg.
public class Student {
#JsonDeserialize(using = SomeAdeserializer.class)
#JsonProperty("dob")
Date dateOfBirth;
}
How can I use a different deserializer for example SomeBdeserializer.class to deserialize my data?
One option is to a have one deserializer which would select the deserilization format in runtime depending on some context information. The selection could be done inside the deserilize method, or the de-serialization could be delegated to another deserializer.
Another option is to register two different deserializers (link) for the same type in two different object mappers. Then select the object mapper in runtime depending on the context. Here is an example:
public class JacksonTwoDeserilizers {
public static class Bean {
public final Date date;
#JsonCreator
public Bean(#JsonProperty("date") Date date) {
this.date = date;
}
#Override
public String toString() {
return "Bean{" +
"date=" + date +
'}';
}
}
public static class DateDeserializer extends StdDeserializer<Date> {
private final int hours;
protected DateDeserializer(int hours) {
super(Date.class);
this.hours = hours;
}
#Override
public Date deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
Date date = this._parseDate(jp, ctxt);
return new Date(date.getTime() + 1000 * 60 * 60 * hours);
}
}
public static void main(String[] args) throws IOException {
ObjectMapper mapper1 = new ObjectMapper();
ObjectMapper mapper2 = new ObjectMapper();
SimpleModule module1 = new SimpleModule();
SimpleModule module2 = new SimpleModule();
module1.addDeserializer(Date.class, new DateDeserializer(6));
mapper1.registerModule(module1);
module2.addDeserializer(Date.class, new DateDeserializer(12));
mapper2.registerModule(module2);
long currentTimeMillis = System.currentTimeMillis();
System.out.println(new Date(currentTimeMillis));
String json = "{\"date\":" + currentTimeMillis + "}";
System.out.println("Mapper1: " + mapper1.readValue(json, Bean.class));
System.out.println("Mapper2: " + mapper2.readValue(json, Bean.class));
}
}
Output:
Sat May 24 12:06:25 CEST 2014
Bean{date=Sat May 24 18:06:25 CEST 2014}
Bean{date=Sun May 25 00:06:25 CEST 2014}