How can I get MOXy to unmarshal JSON into LocalDate and LocalDateTime?
I've got an #GET method which produces a sample instance with three fields of types LocalDate, LocalDateTime and Date, respectively.
Hitting that endpoint, I get:
{
"localDate": "2017-07-11",
"localDateTime": "2017-07-11T10:11:10.817",
"date": "2017-07-11T10:11:10.817+02:00"
}
I then POST the above data to my #POST method, which simply returns the data again:
{
"date": "2017-07-11T10:11:10.817+02:00"
}
As you can see, both localDate and localDateTime are lost in the process, because MOXy does not initialize those two fields.
What gives? MOXy seems to support serialization of these types, but not deserialization?
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Date;
#Path("/test/date")
public class DateTest {
public static class Data {
public LocalDate localDate;
public LocalDateTime localDateTime;
public Date date;
}
#GET
#Path("roundtrip")
public Response roundtrip() {
Data sample = getSample();
return roundtrip(sample);
}
#POST
#Path("roundtrip")
#Consumes(MediaType.APPLICATION_JSON)
public Response roundtrip(Data t) {
return Response.status(Response.Status.OK).entity(t).build();
}
protected Data getSample() {
final Data data = new Data();
data.localDate = LocalDate.now();
data.localDateTime = LocalDateTime.now();
data.date = new Date();
return data;
}
}
Moxy version: jersey-media-moxy-2.25.1
According to peeskillet's suggestion I implemented the following adapter class:
public class LocalDateTimeAdapter extends XmlAdapter<String, LocalDateTime>{
private static final DateTimeFormatter DTF = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
#Override
public String marshal(LocalDateTime localDateTime) throws Exception {
return localDateTime.format(DTF);
}
#Override
public LocalDateTime unmarshal(String string) throws Exception {
return LocalDateTime.parse(string, DTF);
}
}
In addition, I created package-info.java in the same package where my classes for MOXy and the adapter (in a subpackage) are located with the following content:
#XmlJavaTypeAdapters({
#XmlJavaTypeAdapter(type=LocalDateTime.class,
value=LocalDateTimeAdapter.class)
})
package api;
import java.time.LocalDateTime;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapters;
import api.adapter.LocalDateTimeAdapter;
Thus, marshalling and unmarshalling works without problems. And with DTF you can specify the format that shall be applied.
Related
I'm trying to understand what Gson is doing here.
Here's my simple test case.
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import org.junit.Test;
import java.io.IOException;
import java.time.ZoneId;
public class TestSuite
{
public class TestItem
{
private ZoneId TimeZone = ZoneId.of("America/New_York");
}
class ZoneIdAdapter extends TypeAdapter<ZoneId>
{
#Override
public void write(final JsonWriter jsonWriter, final ZoneId timeZone) throws IOException
{
jsonWriter.value(timeZone.getId());
}
#Override
public ZoneId read(final JsonReader jsonReader) throws IOException
{
return ZoneId.of(jsonReader.nextString());
}
}
#Test
public void canSerializeTestItem()
{
Gson gson = new GsonBuilder()
.registerTypeAdapter(ZoneId.class, new ZoneIdAdapter())
.create();
gson.toJson(new TestItem());
}
}
For me using Java 17 and Gson 2.10 that fails with the following:
com.google.gson.JsonIOException: Failed making field 'java.time.ZoneRegion#id' accessible; either increase its visibility or write a custom TypeAdapter for its declaring type.
I'm failing to understand why gson is concerning itself with java.time.ZoneRegion#id when I thought* I already told it how to serialize a ZoneId (I'm guessing ZoneRegion is some internal to to java.time.ZoneId? I can't seem to find it.).
And what's more confusing to me, is that if I change to the following:
public class TestItem
{
#Expose
private ZoneId timeZone = ZoneId.of("America/New_York");
}
// ... omitted other stuff
Gson gson = new GsonBuilder()
.excludeFieldsWithoutExposeAnnotation()
.registerTypeAdapter(ZoneId.class, new ZoneIdAdapter())
.create();
gson.toJson(new TestItem());
It then works as I would expect it to (doesn't error).
Is there a better way to 'teach' gson how to serialize the third party types I'm concerned with?
The actual concrete type that ZoneId.of() returns is a ZoneRegion, and registerTypeAdapter only registers an adapter for a single type.
As ZoneRegion is not a public class you can't (easily) register an adapter for it -- and of course you want a configuration which works even if you have an instance of TestItem where timeZone has a different concrete type.
You can use registerTypeHierarchyAdapter instead. That registers an adapter for a class and all of its subclasses.
I don't know why #Expose changes the behaviour.
I'm building a series of linked classes whose instances I want to be able to marshall to XML so I can save them to a file and read them in again later.
At present I'm using the following code as a test case:
import javax.xml.bind.annotation.*;
import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Marshaller;
import java.time.LocalDate;
public class LocalDateExample
{
#XmlRootElement
private static class WrapperTest {
public LocalDate startDate;
}
public static void main(String[] args) throws JAXBException
{
WrapperTest wt = new WrapperTest();
LocalDate ld = LocalDate.of(2016, 3, 1);
wt.startDate = ld;
marshall(wt);
}
public static void marshall(Object jaxbObject) throws JAXBException
{
JAXBContext context = JAXBContext.newInstance(jaxbObject.getClass());
Marshaller marshaller = context.createMarshaller();
marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE);
marshaller.marshal(jaxbObject, System.out);
}
}
The XML output is:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<wrapperTest>
<startDate/>
</wrapperTest>
Is there a reason why the startDate element is empty? I would like it to contain the string representation of the date (i.e. toString()). Do I need to write some code of my own in order to do this?
The output of java -version is:
openjdk version "1.8.0_66-internal"
OpenJDK Runtime Environment (build 1.8.0_66-internal-b17)
OpenJDK 64-Bit Server VM (build 25.66-b17, mixed mode)
You will have to create an XmlAdapter like this:
public class LocalDateAdapter extends XmlAdapter<String, LocalDate> {
public LocalDate unmarshal(String v) throws Exception {
return LocalDate.parse(v);
}
public String marshal(LocalDate v) throws Exception {
return v.toString();
}
}
And annotate your field using
#XmlJavaTypeAdapter(value = LocalDateAdapter.class)
See also javax.xml.bind.annotation.adapters.XmlJavaTypeAdapters if you want to define your adapters on a package level.
http://blog.bdoughan.com/2011/05/jaxb-and-joda-time-dates-and-times.html describes the hole setup.
Joda-Time provides an alternative to the Date and Calendar classes currently provided in Java SE. Since they are provided in a separate library JAXB does not provide a default mapping for these classes.
To register the adapter for all files in a package. you can add package-info.java in the package you want to register it.
#XmlJavaTypeAdapters({
#XmlJavaTypeAdapter(type=LocalDate.class,
value=LocalDateAdapter.class),
})
package PACKAGE_NAME;
import java.time.LocalDate;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapters;
The adapter looks like:
import javax.xml.bind.annotation.adapters.XmlAdapter;
import java.time.LocalDate;
public class LocalDateAdapter extends XmlAdapter<String, LocalDate>{
public LocalDate unmarshal(String v) throws Exception {
return LocalDate.parse(v);
}
public String marshal(LocalDate v) throws Exception {
return v.toString();
}
}
As Nikolay Antipov mentioned in this commment there is already a well-tested threeten library that provides 14 type adapters (as of 2021-04-26) where one of them is the:
LocalDateXmlAdapter
and can be used e.g. like this on some Java field (generally not recommended by me though):
#XmlJavaTypeAdapter(value = LocalDateXmlAdapter.class) myLocalDt;
Instead I would recommend the package-info.java approach to do it implicitly in each package on the package level so it is applied automagically to e.g. all fields of the LocalDate type in all package classes:
create a file named src/java/my/xmlconv/classes/package-info.java
its content:
#XmlJavaTypeAdapters({
#XmlJavaTypeAdapter(value=LocalDateTimeXmlAdapter.class, type=LocalDateTime.class)
})
package my.xmlconv.classes;
import java.time.LocalDateTime;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapters;
import io.github.threetenjaxb.core.LocalDateTimeXmlAdapter;
I have a Spring Boot controller with param #PathVariable long stopPointId, when user will be send request like "url/StopPoints/1" everything work perfect, but when request will be look like "url/StopPoints/StopPointNumber" nothing will be happening. I want to catch this situation and throw my custom error because user need to know, that param only take long value. For instance: You are not able to pass String as parameter. You should use number value e.g 123."
One way would be to handle the NumberFormatException that would be thrown by Spring Boot while trying to typecast a String into a Long.
This is my custom HTTP-Response class, but I trust you have your own...
package com.stackoverflow.rav.StackOverflowExamples.api;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.http.HttpStatus;
import java.util.Date;
public class HttpResponse {
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "MM-dd-yyyy hh:mm:ss", timezone = "UTC")
private Date timeStampUTC;
private int httpStatusCode;
private HttpStatus httpStatus;
private String reason;
private String message;
/* Include setters, getters, constructors, or use Lombok */
}
Then the exception handler... (exception message should be generic in order to increase reusability)
package com.stackoverflow.rav.StackOverflowExamples.api;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
#RestControllerAdvice
public class ExampleExceptionHandler {
#ExceptionHandler(NumberFormatException.class)
public ResponseEntity<HttpResponse> accountDisabledException() {
return createHttpResponse(HttpStatus.BAD_REQUEST, "Should pass long not string!");
}
private ResponseEntity<HttpResponse> createHttpResponse(HttpStatus httpStatus, String message) {
return new ResponseEntity<>(new HttpResponse(
httpStatus.value(),
httpStatus,
httpStatus.getReasonPhrase().toUpperCase(),
message.toUpperCase()),
httpStatus);
}
}
Finally the controller...
package com.stackoverflow.rav.StackOverflowExamples.api;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class ExampleController extends ExampleExceptionHandler {
#GetMapping("/url/{someValue}")
public String hello(#PathVariable("someValue") long someValue) {
return "You entered: " +someValue;
}
}
If all goes well you should get a response like the screen-snippet below when doing http://localhost:8080/url/abcd
This answer might look lengthy, but we are Java developers :D
Let's say I have a POST request with attachment in using Jersey Framework.
From my Jersey endpoint, i get a FormDataBodyPart, which contains that json string:
{
"aField": "aValue",
"aSecondField":"anotherValue",
"collectionDate" : {
"firstDate" : "2019-07-15",
"secondDate" : "2019-07-15T00:00:00.000Z"
}
}
When I deserialize that json into a Java Object, the firstDate field reflect another date because it didnt have a timezone (2019-07-14T22:00:00.000Z).
formDataBodyPartData.setMediaType(MediaType.APPLICATION_JSON_TYPE);
EntityExample entityExample = formDataBodyPartData.getValueAs(EntityExample.class);
So I'd like to force users to put the timezone information in each date.
Is it possible ? (I can have a solution based on Jackson too).
Thanks in advance.
EDIT :
The collectionDate class has the following schema and unfortunately I can't change it.
public class CollectionDate{
private java.util.Date firstDate;
private java.util.Date secondDate;
....
}
So I'd like a solution in order to restrict the unmarschalling of the json string when i use that line :
formDataBodyPartData.getValueAs(..)
I can have as well a solution based on Jackson, for instance :
EntityExample entityExample = objectMapper.readValue(value, EntityExample.class);
Thanks in advance.
You could use OffsetDateTime for mapping both firstDate and secondDate properties. The deserialization will fail when the input doesn't match the DateTimeFormatter.ISO_OFFSET_DATE_TIME pattern:
#Data
public class CollectionDate {
private OffsetDateTime firstDate;
private OffsetDateTime secondDate;
}
To use Jackson as JSON provider for JAX-RS, you must add the jackson-jaxrs-json-provider artifact as dependency.
And you will also need to register the JavaTimeModule in the ObjectMapper. It can be done in a ContextResolver<T>:
#Provider
public class ObjectMapperContextResolver implements ContextResolver<ObjectMapper> {
private final ObjectMapper mapper;
public ObjectMapperContextResolver() {
this.mapper = createObjectMapper();
}
#Override
public ObjectMapper getContext(Class<?> type) {
return mapper;
}
private ObjectMapper createObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
return mapper;
}
}
Create a custom deserializer is the main solution but it doesnt fit to my project.
Anyway, this solution works :
Create a custom deserializer like below :
import java.io.IOException;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
public final class FlexDateDeserialiser extends JsonDeserializer<Date> {
#Override
public Date deserialize(final JsonParser parser, final DeserializationContext context) throws IOException {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX");
final String date = parser.getText();
try {
return formatter.parse(date);
} catch (final ParseException ex) {
throw new IOException("Exemple date");
}
}
}
https://www.baeldung.com/jackson-serialize-dates
http://www.leveluplunch.com/java/tutorials/033-custom-jackson-date-deserializer/
We can use that custom deserializer directly in the POJO :
public class CollectionDate{
#JsonDeserialize(using = FlexDateDeserialiser.class) private java.util.Date firstDate;
#JsonDeserialize(using = FlexDateDeserialiser.class) private java.util.Date secondDate;
....
}
I have a RESTFul API consuming/returning JSON in the request/response body. When the client sends invalid data (valid JSON but invalid values for the fields) I want to be able to return a JSON structure (as well as the relevant 400+ code).
This structure would then allow the frontend to parse the errors on a per-field basis and render the errors alongside the input fields.
E.g. ideal output:
{
"errors":{
"name":["invalid chars","too long","etc"]
"otherfield":["etc"]
}
}
I am using Resteasy for the API, and using violation exceptions it's fairly easy to get it to render JSON errors:
#Provider
#Component
public class ValidationExceptionHandler implements ExceptionMapper<ResteasyViolationException> {
public Response toResponse(ResteasyViolationException exception) {
Multimap<String,String> errors = ArrayListMultimap.create();
Consumer<ResteasyConstraintViolation> consumer = (violation) -> {
errors.put(violation.getPath(), violation.getMessage());
};
exception.getParameterViolations().forEach(consumer);
Map<String, Map<String, Collection<String>>> top = new HashMap<>();
top.put("errors", errors.asMap());
return Response.status(Status.BAD_REQUEST).entity(top)
.build();
}
}
However, the error paths (violation.getPath()) are property-centric rather than XmlElement-name-centric.
E.g. the above outputs:
{
"errors":{"createCampaign.arg1.name":["invalid chars","etc"]}
}
I have tried stripping the index back from the last dot to get "name" but there are other issues with that hack.
E.g. if my "name" property isn't "name" it doesn't work:
#XmlElement(name="name")
#NotNull
private String somethingelse;
"somethingelse" will be returned to client, but they client has no idea what that is:
{
"errors":{"somethingelse":["cannot be null"]}
}
The client wants "name" since that is what the field was called when they sent it.
My resource:
package com.foo.api;
import org.springframework.stereotype.Service;
import javax.validation.Valid;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import com.foo.dto.CarDTO;
#Service
#Path("/car")
public class CarResource {
#POST
#Produces({MediaType.APPLICATION_JSON})
#Consumes(MediaType.APPLICATION_JSON)
public CarDTO create(
#Valid CarDTO car
) {
//do some persistence
return car;
}
}
example dto:
package com.foo.dto;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Min;
import javax.validation.constraints.Max;
import javax.xml.bind.annotation.XmlElement;
public class CarDTO {
#Min(1)
#Max(10)
#NotNull
#XmlElement(name="gears")
private int cogs;
}
This article describes quite well what you need to do.
Basically you should implement an ExceptionMapper.
#Provider
public class ValidationExceptionMapper implements ExceptionMapper<ValidationException> {
#Override
public Response toResponse(ValidationException exception) {
Response myResponse;
// build your Response based on the data provided by the exception
return myResponse;
}
}
A custom error message can be used so you wouldnt need to look at the path
#NotNull(message="name cannot be null")