3

Here is a simplified version of my problem. Consider the following REST services...

@Stateless
@Produces(MediaType.APPLICATION_JSON)
@Consumes(MediaType.APPLICATION_JSON)
@Path("/test")
public class TestResource {


    @POST
    public Response test(Calendar c) {
        System.out.println(c);
        return Response.ok(Response.Status.OK).build();
    }

    @GET
    @Path("/getCalendar")
    public Response getCalendar() {
        return Response.ok(toJSONString(Calendar.getInstance())).build();
    }

    public String toJSONString(Object object) {
        GsonBuilder builder = new GsonBuilder();
        builder.setDateFormat("yyy-MM-dd'T'HH:mm:ss.SSS'Z'");
        Gson gson = builder.create();
        return gson.toJson(object);
    }
}

and of course, this...

@ApplicationPath("/rest")
public class RestActivator extends Application {

}

I am using Postman, and at first I get a JSON representation of the Calendar object by sending a GET request to 'http://localhost:8080/ProjectName/rest/test/getCalendar'. I then copy the JSON that gets returned which is

{
  "year": 2015,
  "month": 5,
  "dayOfMonth": 29,
  "hourOfDay": 10,
  "minute": 7,
  "second": 24
}

Then using Postman I send a POST to 'http://localhost:8080/ProjectName/rest/' with the data that was returned to me above. Then the 'javax.ws.rs.NotSupportedException: Cannot consume content type' gets thrown.

How can I fix this so the service can handle Calendar objects (and classes that have Calendar objects as fields)?

Edit:

Yes I am setting the correct content type which is application/json.

Here is the response I am getting...

com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of java.util.Calendar out of START_OBJECT token at [Source: io.undertow.servlet.spec.ServletInputStreamImpl@4cf87599; line: 1, column: 1]

Update: To get this to work I used @peeskillets solution.

Rob L
  • 3,073
  • 6
  • 31
  • 61
  • are you setting a 'content-type=application/json' on the REST client? – SpaceCowboy Jun 29 '15 at 14:23
  • " I then copy the JSON that gets returned" Can you show us how you are copying the data? The only thing I can think is that the JSON data is invalid when it's sent. It might be worth using Chromes Developer mode to see what you are actually sending... – SpaceCowboy Jun 29 '15 at 14:29
  • You're probably going to need a custom deserizalizer – Paul Samsotha Jun 29 '15 at 14:31
  • @SpaceCowboy I posted the JSON I sent with the request in the question. Like I stated in the question, I do the GET request which returns a JSON representation of Calendar and then I send that data as the POST. – Rob L Jun 29 '15 at 14:34
  • @peeskillet Can you please elaborate some more? – Rob L Jun 29 '15 at 14:36
  • Based on error message, JSON Object is sent in position where `Calendar` instance would be deserialized. This is wrong: value must be a JSON String (with ISO-8601 representation) or JSON number with 64-bit timestamp. – StaxMan Jun 29 '15 at 22:56

4 Answers4

3

Jackson generally only works with JavaBean style POJOs, which Calendar is not. For these cases, Jackson allows us to create custom Deserializers. Once we create the deserializer, we can register it with the ObjectMapper in a ContextResolver. For example

public class CalendarDeserializer extends JsonDeserializer<Calendar>  {

    @Override
    public Calendar deserialize(JsonParser jp, DeserializationContext dc) 
            throws IOException, JsonProcessingException {
        JsonNode node = jp.getCodec().readTree(jp);
        int year = getInt("year", node);
        int month = getInt("month", node);
        int dayOfMonth = getInt("dayOfMonth", node);
        int hourOfDay = getInt("hourOfDay", node);
        int minute = getInt("minute", node);
        int second = getInt("second", node);

        Calendar c = Calendar.getInstance();
        c.set(year, month, dayOfMonth, hourOfDay, minute, second);
        return c;
    }

    private int getInt(String name, JsonNode node) {
        return (Integer) ((IntNode) node.get(name)).numberValue();
    } 
}

To register it with the ObjectMapper we can do

ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(Calendar.class, new CalendarDeserializer());
mapper.registerModule(module);

To use the ObjectMapper with our JAX-RS application, we can create it in the ContextResolver, as seen here. You will need to add the jackson-databind as a dependency, if you don't already have it.

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>${jackson2.version}</version>
</dependency>

Note that the above dependency already comes with Jackson JSON providers for JAX-RS, so if you already have Jackson JSON support dependency, you won't need it.

I've tested this with the JSON you have provided, and it works fine.

See more about custom deserializers here

Community
  • 1
  • 1
Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • Jackson does actually come equipped with serializers, deserializers for most JDK datatypes that 1.6 contains, and both `java.util.Date` and `java.util.Calendar` are supported out of the box. However, it is recommended that the root-level value is indeed POJO and not a scalar value (like JSON String that `Calendar` is typically serailized as); this both for JSON spec compliancy (root value should be Object or Array, as per JSON spec!), and for other issues related to type safety (generic Maps and Lists can be problematic) – StaxMan Jun 29 '15 at 18:03
  • I will select this as the answer, however I guess I am still a little confused where this `ObjectMapper mapper = new ObjectMapper(); SimpleModule module = new SimpleModule(); module.addDeserializer(Calendar.class, new CalendarDeserializer()); mapper.registerModule(module);` will go inside my application? That is, I am not sure where I will put that code. I am completely new to all of this... thanks for your patience. – Rob L Jun 29 '15 at 22:33
  • @StaxMan - Can you please explain how I can use these Jackson features in my app? – Rob L Jun 29 '15 at 22:36
  • It just works automatically; but I see couple of problems there. First, on get method, you are for some reason doing manually conversion using Gson. That should not be needed. But bigger problem is that you are trying to pass `Calendar` as JSON Object: this will not work. Pass it either as JSON String, or as timestamp (64-bit JSON Number), and that'll work. Use of custom (de)serializer is not needed at all. – StaxMan Jun 29 '15 at 22:55
  • @TryCatch did you look at the link about the ContextResolver? – Paul Samsotha Jun 29 '15 at 23:44
  • @peeskillet Dude, thank you so much. That worked. I will be honest, I am a complete beginner with all of this so thank you for your patience. – Rob L Jun 30 '15 at 00:57
  • Please do note that I share the same concerns as StaxMan. 1) Is a JSON object really what you want to return to represent the Calendar? Not a date String? It seems that's what you are trying to accomplish with the data format. 2) Do we really expect the client the send this JSON object for Calendar? 3) Why a Calendar alone by itself at all, without any surrounding context. A lot if it doesn't make sense. My answer simply solves the direct problem of your question, but maybe doesn't solve the overall "design" problem. – Paul Samsotha Jun 30 '15 at 01:56
1

You need to pass in proper content type in request and check whether you are giving right path..

ManojP
  • 6,113
  • 2
  • 37
  • 49
0

So after doing some research and thanks to those who answered ( specifically @peeskillet ) I have come up with 2 solutions to this problem.

Solution 1

Go with @peeskillet 's answer and create a custom deserializer. That worked, and here are the 2 classes I added to get that to work...

import java.util.Calendar;

import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Provider;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;

@Provider
public class ObjectMapperContextResolver implements ContextResolver<ObjectMapper> {

    private final ObjectMapper mapper;

    public ObjectMapperContextResolver() {

        mapper = new ObjectMapper();
        SimpleModule module = new SimpleModule();
        module.addDeserializer(Calendar.class, new CalendarDeserializer());
        mapper.registerModule(module);
        mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
   }

    @Override
    public ObjectMapper getContext(Class<?> type) {
        return mapper;
    }
}

as well as the custom Deserializer

import java.io.IOException;
import java.util.Calendar;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.IntNode;

public class CalendarDeserializer extends JsonDeserializer<Calendar>  {

    private int getInt(String name, JsonNode node) {
        return (Integer) ((IntNode) node.get(name)).numberValue();
    }

    @Override
    public Calendar deserialize(JsonParser jp, com.fasterxml.jackson.databind.DeserializationContext dc) throws IOException,
            JsonProcessingException {
        JsonNode node = jp.getCodec().readTree(jp);
        int year = getInt("year", node);
        int month = getInt("month", node);
        int dayOfMonth = getInt("dayOfMonth", node);
        int hourOfDay = getInt("hourOfDay", node);
        int minute = getInt("minute", node);
        int second = getInt("second", node);

        Calendar c = Calendar.getInstance();
        c.set(year, month, dayOfMonth, hourOfDay, minute, second);
        return c;
    } 
}

Solution 2

However, I also figured out another solution that may be easier. If I omitted the GSON toJSONString (not sure why I was mixing Jackson and GSON...) in the @GET method and simply returned Response.ok(Calendar.getInstance()).build();, Jackson converted the Calendar object to it's primitive long value. When I passed in a long value to the @POST method with a Calendar object as the method parameter, Jackson was able to deserialize it properly as a Calendar object.

Conclusion

If you want to pass JSON representations of Calendar objects to a REST endpoint, pass it as the long representation.

On the other hand you can always create a custom deserializer if you have a JavaScript Date object or a custom Date object.

Rob L
  • 3,073
  • 6
  • 31
  • 61
0

I know this is old question, but I just added another constructor with long instead of Calendar so it could deserialize

@POST
public Response test(long c) {
    System.out.println(c);
    return Response.ok(Response.Status.OK).build();
}

For Example:

public class demo{

    String id;
    Calendar date;
    String tag;

    public demo(Calendar date, String tag) {
        this.date = date;
        this.tag = tag;
    }

    public demo(long date, String tag) {
        this.date = longToCalendar(date);
        this.tag = tag;
    }

    public Calendar longToCalendar(Long epoch) {
        Calendar calendar = Calendar.getInstance();
        calendar.setTimeInMillis(epoch);
        return calendar;
    }
}


@POST
public Response test(Demo demo) {
    System.out.println(demo);
    return Response.ok(Response.Status.OK).build();
}
ASH
  • 980
  • 1
  • 9
  • 22