I am trying to persist a java object having java.util.Date field in mongo collection using fasterxml jackson.
The problem is the default nature of objectMapper is to store Date as NumberLong type.
For e.g , a createdTime field of java.util.Date type gets stored as below:
"createdTime" : NumberLong("1427728445176")
I want to store it in ISODate format which is available in mongo Shell.
Now, i know there is way to format object mapper to store Date in a String dateformat.
But I am ONLY looking for ISODate() format.
For e.g
"createdTime" : ISODate("2015-01-20T16:39:42.132Z")
Is there a way to do that ?
Please advise gurus .
Thanks in advance for help.
What you need is the Jackson Joda Module. If you import that into your classpath, you can do the following on your mapper to write it as your desired Timestamp:
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JodaModule());
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true);
mapper.writeValueAsString(date);
You can replace date in the code sample above with your POJO as necessary.
Edit:
It looks like what you really want is a custom serializer. That would look something like this:
public class IsoDateSerializer extends JsonSerializer<DateTime> {
#Override
public void serialize(DateTime value, JsonGenerator jgen, SerializerProvider provider) {
String isoDate = ISODateTimeFormat.dateTime().print(value);
jgen.writeRaw("ISODATE(\"" + isoDate + "\")");
}
Then you'll either register it on the mapper for all DateTime types
mapper.addSerializer(DateTime.class, new IsoDateSerializer());
or specify it on the function using annotations
#JsonSerializer(using = IsoDateSerializer.class)
public DateTime createdTime;
I was able to serialize the date string to ISODate format. I wrote a customer date serializer like below.
public void serialize(Date date, JsonGenerator jgen, SerializerProvider provider) throws IOException {
String dateValue = getISODateString(date);
String text = "{ \"$date\" : \""+ dateValue +"\"}";
jgen.writeRawValue(text);
}
Based on request from user #mmx73, I am adding code for customer Date DeSeriaizer.
public class IsoDateDeSerializer extends JsonDeserializer<Date> {
#Override
public Date deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException, JsonProcessingException {
ObjectCodec oc = jsonParser.getCodec();
JsonNode node = oc.readTree(jsonParser);
String dateValue = node.get("$date").asText();
//DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
DateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
Date date = null;
try {
date = df.parse(dateValue);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
}
None of these answers accomplished what I wanted. I was having trouble because when I serialized the JSON string to MongoDB, it was stored as a String. A nicely formatted string, but a string none the less.
I use the com.fasterxml.jackson.databind.ObjectMapper to convert my objects to/from JSON and I wanted to continue using this class. I have the following method:
public enum JsonIntent {NONE, MONGODB};
public static ObjectMapper getMapper(final JsonIntent intent) {
ObjectMapper mapper = new ObjectMapper();
// Setting to true saves the date as NumberLong("1463597707000")
// Setting to false saves the data as "2016-05-18T19:30:52.000+0000"
mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
mapper.registerModule(new JodaModule());
if (intent == JsonIntent.MONGODB) {
// If you want a date stored in MONGO as a date, then you must store it in a way that MONGO
// is able to deal with it.
SimpleModule testModule = new SimpleModule("MyModule", new Version(1, 0, 0, null, null, null));
testModule.addSerializer(Date.class, new StdSerializer<Date>(Date.class) {
private static final long serialVersionUID = 1L;
#Override
public void serialize(Date value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
try {
if (value == null) {
jgen.writeNull();
} else {
jgen.writeStartObject();
jgen.writeFieldName("$date");
String isoDate = ISODateTimeFormat.dateTime().print(new DateTime(value));
jgen.writeString(isoDate);
jgen.writeEndObject();
}
} catch (Exception ex) {
Logger.getLogger(JsonUtil.class.getName()).log(Level.SEVERE, "Couldn't format timestamp " + value + ", writing 'null'", ex);
jgen.writeNull();
}
}
});
testModule.addDeserializer(Date.class, new StdDeserializer<Date>(Date.class) {
private static final long serialVersionUID = 1L;
#Override
public Date deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
JsonNode tree = jp.readValueAsTree();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
try {
return ISODateTimeFormat.dateTime().parseDateTime(tree.get("$date").textValue()).toDate();
} catch (Throwable t) {
throw new IOException(t.getMessage(), t);
}
}
});
mapper.registerModule(testModule);
}
return mapper;
}
Now, I can run the following test code:
BObjectMapper mapper = getMapper(JsonUtil.JsonIntent.NONE);
Date d1 = new Date();
String v = mapper.writeValueAsString(d1);
System.out.println("Joda Mapping: " + v);
Date d2 = mapper.readValue(v, Date.class);
System.out.println("Decoded Joda: " + d2);
mapper = getMapper(JsonUtil.JsonIntent.MONGODB);
v = mapper.writeValueAsString(d1);
System.out.println("Mongo Mapping: " + v);
d2 = mapper.readValue(v, Date.class);
System.out.println("Decoded Mongo: " + d2);
The output is as follows:
Joda Mapping: "2016-06-13T14:58:11.937+0000"
Decoded Joda: Mon Jun 13 10:58:11 EDT 2016
Mongo Mapping: {"$date":"2016-06-13T10:58:11.937-04:00"}
Decoded Mongo: Mon Jun 13 10:58:11 EDT 2016
Note that the JSON that will be sent to MONGODB defines the value containing a field named "$date". This tells MongoDB that this is a date object it seems.
When I look at Mongo, I see the following:
"importDate" : ISODate("2016-05-18T18:55:07Z")
Now, I can access the field as a date rather than as a string.
To add an encoded JSON string to Mongo, my code is as follows:
MongoDatabase db = getDatabase();
Document d = Document.parse(json);
db.getCollection(bucket).insertOne(d);
In this case, "json" is the encoded JSON string. Because it is coming from a JSON string, it has no way of knowing the types unless it infers this, which is why we needed the "$date" portion. The "bucket" is just a string indicating which table to use.
As a side note, I found out that if I pull a BSON object from Mongo and convert it to a JSON string by calling doc.toJson() (where doc is of type org.bison.Document as returned from a query), the date object is stored with a long value rather than a formatted text string. I did not check to see if I could push data into mongo after formatting in this way, but, you can modify the deserializer shown above to support this as follows:
testModule.addDeserializer(Date.class, new StdDeserializer<Date>(Date.class) {
private static final long serialVersionUID = 1L;
#Override
public Date deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
JsonNode tree = jp.readValueAsTree();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ");
try {
// Mongo will return something that looks more like:
// {$date:<long integer for milliseconds>}
// so handle that as well.
JsonNode dateNode = tree.get("$date");
if (dateNode != null) {
String textValue = dateNode.textValue();
if (!Util.IsNullOrEmpty(textValue)) {
return ISODateTimeFormat.dateTime().parseDateTime(textValue).toDate();
}
return Util.MillisToDate(dateNode.asLong());
}
return null;
} catch (Throwable t) {
Util.LogIt("Exception: " + t.getMessage());
throw new IOException(t.getMessage(), t);
}
}
});
You can convert milliseconds to a Date or DateTime as follows:
/**
* Convert milliseconds to a date time. If zero or negative, just return
* null.
*
* #param milliseconds
* #return
*/
public static Date MillisToDate(final long milliseconds) {
if (milliseconds < 1) {
return null;
}
Calendar calendar = Calendar.getInstance();
calendar.setTimeInMillis(milliseconds);
return calendar.getTime();
}
public static DateTime MillisToDateTime(final long milliseconds) {
if (milliseconds < 1) {
return null;
}
return new DateTime(milliseconds);
}
In case you get some message like
com.fasterxml.jackson.core.JsonGenerationException: Can not write a field name, expecting a value.
Make sure you use writeRawValue instead within the accepted answer. That ends the field correctly, otherwise the next field to be serialize may throw this error.
You can solve this issue by reading/writing bson instead of json. Here is a test class:
package com.nagra.jongo.mapper;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import de.undercouch.bson4jackson.BsonFactory;
import de.undercouch.bson4jackson.deserializers.BsonDateDeserializer;
import de.undercouch.bson4jackson.serializers.BsonDateSerializer;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.Date;
/**
* Uses Bson4Jackson 2.9.0
*
* <!-- https://mvnrepository.com/artifact/de.undercouch/bson4jackson -->
* <dependency>
* <groupId>de.undercouch</groupId>
* <artifactId>bson4jackson</artifactId>
* <version>2.9.2</version>
* </dependency>
*/
public class ObjectMapperTest {
private ObjectMapper mapper = new ObjectMapper(new BsonFactory());
private static class WrappedDate {
private Date Date = new Date(0);
public WrappedDate() {
}
public Date getDate() {
return Date;
}
public void setDate(Date Date) {
this.Date = Date;
}
}
#Before
public void setUp() {
SimpleModule module = new SimpleModule();
module.addSerializer(Date.class, new BsonDateSerializer());
module.addDeserializer(Date.class, new BsonDateDeserializer());
mapper.registerModule(module);
}
#Test
public void testDate() throws IOException {
WrappedDate date = new WrappedDate();
byte[] b = mapper.writeValueAsBytes(date);
WrappedDate i = mapper.readValue(b, WrappedDate.class);
Assert.assertEquals(date.getDate(), i.getDate());
System.out.println(i.getDate());
}}
Related
I am implementing an application that gets the data from certin endponts in json formats and tries to deserialize tem to Java Objects but I have problems with the parsing of the date in the JSON.This is how the Date looks like in the JSON: "/Date(1633122000000+0300)/" and I cannot find information in Google how to successfully parse this format.
{
"Date": "/Date(1633122000000+0300)/",
"Filled": 0,
"Needed": 0,
"Paid": 0
}
This is the pojo I use to deserialize the data to using Jackson ObjectMapper:
import java.util.Date;
#Data
#AllArgsConstructor
#NoArgsConstructor
public class TimeByDateSheet {
#JsonProperty("Date")
#JsonFormat(timezone = "GMT+03:00")
#JsonDeserialize(using = DateDeserializer.class, as=Date.class)
private Date date;
#JsonProperty("Filled")
private Long filled;
#JsonProperty("Needed")
private Long needed;
#JsonProperty("Paid")
private Integer paid;
}
And here is my DateDeserializer:
#SuppressWarnings("serial")
public class DateDeserializer extends JsonDeserializer<Date> {
#Override
public Date deserialize(JsonParser jsonParser, DeserializationContext context)
throws IOException, JsonProcessingException {
SimpleDateFormat simpleDateFormat =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzz", Locale.getDefault());
String dateStr = jsonParser.getText();
Date date;
try{
date = simpleDateFormat.parse(dateStr);
}catch(ParseException e){
throw new RuntimeException(e);
}
return date;
}
}
But it does not work correctly. I get the following exception:
Connected to the target VM, address: '127.0.0.1:52760', transport: 'socket'
SLF4J: Failed to load class "org.slf4j.impl.StaticLoggerBinder".
SLF4J: Defaulting to no-operation (NOP) logger implementation
SLF4J: See http://www.slf4j.org/codes.html#StaticLoggerBinder for further details.
Exception in thread "main" com.fasterxml.jackson.databind.JsonMappingException: java.text.ParseException: Unparseable date: "/Date(1633035600000+0300)/" (through reference chain: java.util.ArrayList[0]->com.dataart.forecasts.pojo.timebydate.TimeByDateSheet["Date"])
at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:392)
at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:351)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.wrapAndThrow(BeanDeserializerBase.java:1821)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:315)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:176)
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer._deserializeFromArray(CollectionDeserializer.java:355)
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:244)
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:28)
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:322)
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4675)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3630)
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3613)
at com.dataart.forecasts.DataProcessor.deserializeTimeByDateSheetsList(DataProcessor.java:198)
at com.dataart.forecasts.ForecastReportApplication.main(ForecastReportApplication.java:50)
Caused by: java.lang.RuntimeException: java.text.ParseException: Unparseable date: "/Date(1633035600000+0300)/"
at com.dataart.forecasts.DateDeserializer.deserialize(DateDeserializer.java:28)
at com.dataart.forecasts.DateDeserializer.deserialize(DateDeserializer.java:16)
at com.fasterxml.jackson.databind.deser.impl.MethodProperty.deserializeAndSet(MethodProperty.java:129)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.vanillaDeserialize(BeanDeserializer.java:313)
... 10 more
Caused by: java.text.ParseException: Unparseable date: "/Date(1633035600000+0300)/"
at java.base/java.text.DateFormat.parse(DateFormat.java:395)
at com.dataart.forecasts.DateDeserializer.deserialize(DateDeserializer.java:26)
... 13 more
Could someone help me, please. I searched a lot in internet but could not find a solution.
Thank you in advance! :)
It looks like there is a problem generating the JSON. I really don't think you want to have the dates formatted like that. Right now, you have some odd text surrounding a unix timestamp in milliseconds followed by a zone offset. You are also using the old and rather frowned-upon Date and SimpleDateFormat classes rather than the newer java.time API. However, it is possible to deserialize your date format. Here is one way:
public class DateDeserializer extends JsonDeserializer<Date> {
#Override
public Date deserialize(JsonParser jsonParser, DeserializationContext context)
throws IOException, JsonProcessingException {
Pattern pattern = Pattern.compile("/Date\\((\\d+)([+-]\\d+)\\)/");
Matcher matcher = pattern.matcher(jsonParser.getText());
if (matcher.find()) {
String timestamp = matcher.group(1);
String offset = matcher.group(2);
Instant instant = Instant.ofEpochMilli(Long.parseLong(timestamp));
ZonedDateTime zdt = instant.atZone(ZoneId.of(offset));
instant = zdt.toInstant();
return Date.from(instant);
} else {
throw new RuntimeException("Invalid format: " + jsonParser.getText());
}
}
}
java.time
For this answer I am assuming:
The time in your JSON may come with or without the UTC offset.
You can go all-in on java.time, the modern Java date and time API, and declare your variable to be of type Instant or OffsetDateTime, for example (not Date).
For JSON that comes with an offset such as +0300 declare your variable an OffsetDateTime. Then use the following deserializer.
public class OdtDeserializer extends JsonDeserializer<OffsetDateTime> {
private static final DateTimeFormatter JSON_DATE_FORMATTER = new DateTimeFormatterBuilder()
.appendLiteral("/Date(")
.appendValue(ChronoField.INSTANT_SECONDS)
.appendValue(ChronoField.MILLI_OF_SECOND, 3)
.appendOffset("+HHMM", "Z")
.appendLiteral(")/")
.toFormatter();
#Override
public OffsetDateTime deserialize(JsonParser jsonParser, DeserializationContext context)
throws IOException {
String dateStr = jsonParser.getText();
return OffsetDateTime.parse(dateStr, JSON_DATE_FORMATTER);
}
}
For JSON that comes without offset like /Date(1636510000000)/ declare your variable Instant. Use a similar deserializer. Leave out the offset from the formatter. Parse into an Instant — the syntax is a bit different.
public class InstantDeserializerWIthoutOffset extends JsonDeserializer<Instant> {
private static final DateTimeFormatter JSON_DATE_FORMATTER = new DateTimeFormatterBuilder()
.appendLiteral("/Date(")
.appendValue(ChronoField.INSTANT_SECONDS)
.appendValue(ChronoField.MILLI_OF_SECOND, 3)
.appendLiteral(")/")
.toFormatter();
#Override
public Instant deserialize(JsonParser jsonParser, DeserializationContext context)
throws IOException {
String dateStr = jsonParser.getText();
return JSON_DATE_FORMATTER.parse(dateStr, Instant::from);
}
}
For JSON that may come with or without the offset still use Instant and just modify the formatter of the latter deserializer to include an optional offset:
private static final DateTimeFormatter JSON_DATE_FORMATTER = new DateTimeFormatterBuilder()
.appendLiteral("/Date(")
.appendValue(ChronoField.INSTANT_SECONDS)
.appendValue(ChronoField.MILLI_OF_SECOND, 3)
.optionalStart()
.appendOffset("+HHMM", "Z")
.optionalEnd()
.appendLiteral(")/")
.toFormatter();
If you cannot modify your POJO class and need to stay with Date, modify my Instant deserializer into a Date deserializer by changing the declaration and returning a Date like this:
String dateStr = jsonParser.getText();
Instant inst = JSON_DATE_FORMATTER.parse(dateStr, Instant::from);
return Date.from(inst);
Final solution catching both: /Date(1633035600000+0300)/ and /Date(-62135596800000)/ (the latter was also present at one place in oneof the JSONs). Thank you #DavidConrad
#Override
public Date deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException, JsonProcessingException {
String dateString = jsonParser.getText();
Pattern pattern = Pattern.compile("/Date\\((-)?(\\d+)([+-]\\d+)?\\)/");
Matcher matcher = pattern.matcher(dateString);
if (!matcher.find()) {
throw new RuntimeException("Invalid format: " + dateString);
}
String timestamp = matcher.group(2);
String offset = matcher.group(3);
Instant instant = Instant.ofEpochMilli(Long.parseLong(timestamp));
if (nonNull(offset)) {
ZonedDateTime zdt = instant.atZone(ZoneId.of(offset));
instant = zdt.toInstant();
}
return Date.from(instant);
}
#DavidConrad Thank you, I will try your solution.
By the way, for now I made a workaround that works for me for now:
#Override
public Date deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException, JsonProcessingException {
SimpleDateFormat dateFormattter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String dateStr = jsonParser.getText();
String timeZone = dateStr.substring(dateStr.indexOf("+") + 1, dateStr.indexOf(")"));
String timeZoneShift = String.format("%s:%s",
timeZone.substring(0, timeZone.length()/2),
timeZone.substring(timeZone.length()/2));
dateFormattter.setTimeZone(TimeZone.getTimeZone(String.format("GMT+%s", timeZoneShift)));
Long millis = 0L;
if (dateStr.contains("+") && !dateStr.contains("-")) {
millis = Long.parseLong(dateStr.substring(dateStr.indexOf("(") + 1, dateStr.indexOf("+")));
} else if (dateStr.contains("+") && !dateStr.contains("-")) {
millis = Long.parseLong(dateStr.substring(dateStr.indexOf("(") + 1, dateStr.indexOf(")")));
}
Date date = new Date(millis);
String stringDate= dateFormattter.format(date);
try {
date = dateFormattter.parse(stringDate);
} catch (ParseException e) {
throw new RuntimeException(e);
}
return date;
}
I have a use case where I get the object as a json string and consecutively needs to transform it into a HashMap. My code is as follows:
public Map<String, Object> toMap(String jsonString) {
Gson gson = new Gson();
Type type = new TypeToken<Map<String, Object>>() {
}.getType();
Map<String, Object> mapped = gson.fromJson(jsonString, type);
return mapped;
}
The date value I get from jsonString is "date": "2018-07-29T23:52:35.814Z" but upon serialization into HashMap, the "date" value is a String and not a Date object. Is there a way around it? Even solutions where Gson is not used is welcome
Sample jsonString is as follows:
{
"id": "1351",
"date": "2018-07-30T00:32:31.564Z",
"university": "US",
"typeofwork": "Report",
"title": "Thesis title",
"subject": "Masters",
"noofwords": "123"
}
To clarify, I am not getting any errors with the serialization/deserialization per se. I just want the date value to be of type java.util.Date such that a validation of if(map.get("date") instanceOf java.util.Date) will return true
If you have known exactly the property "date" is a Date, after parsing from Json, you can try somethiing like:
String dateStr = mapped.get("date");
mapped.put("date",new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").parse(dateStr));
If you don't want to do it manually, simply define a class that matches your Json object format (with the "date" field declared as a Date object), then:
Gson g = new GsonBuilder().setDateFormat("your date format").create();
NewClass obj = g.fromJson(jsonStr, NewClass.class);
Gson will parse the date string follow the format in setDateFormat() method.
You can do it using customDeserialzer class in Jackson:
public class CustomDeserializer extends JsonDeserializer {
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
if(p.getCurrentName().equals("date")){
try {
return new SimpleDateFormat("yyyy-MM-dd'T'hh:mm:ss.SSS").parse(p.getText());
}catch (Exception ex){
}
return p.getText();
}
return p.getText();
}
}
Then parse like it:
ObjectMapper objectMapper = new ObjectMapper();
Map<String, Object> hashMap = new HashMap<>();
String json = "{\"date\": \"2018-07-29T23:52:35.814Z\"}";
SimpleModule module = new SimpleModule().addDeserializer(String.class, new CustomDeserializer());
objectMapper.registerModule(module);
hashMap = objectMapper.readValue(json,new TypeReference<HashMap<String,Object>>(){});
hashMap.entrySet().parallelStream().forEach(e -> System.out.println(e.getValue()));
I created a custom Date deserializer:
private static class DateJsonDeserializer implements JsonDeserializer<Date>{
#Override
public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
for (String format : DATE_FORMATS) {
try {
SimpleDateFormat df = new SimpleDateFormat(format);
return df.parse(json.getAsString());
} catch (ParseException e) {
}
}
throw new JsonParseException("Unparseable date: \"" + json.getAsString()
+ "\". Supported formats: " + Arrays.toString(DATE_FORMATS));
}
}
I added this to my gsonBuilder:
gsonBuilder.registerTypeAdapter(Date.class, new DateJsonDeserializer());
But if I would like to parse a date I see the built in DateTypeAdapter class used, why? I would like to use my date deserializer.
I use retrofit 2.1.0, Gson 2.8.0, converter-gson 2.1.0
are you sure that Date.class is the correct class? Maybe it uses a subclass of Date.class ?
I am using the Jackson (1.9.x) library to parse JSON into a Map:
ObjectMapper mapper = new ObjectMapper();
Map<String,Object> map = (Map<String,Object>) mapper.readValue(jsonStr, Map.class);
Is there a way to tell the Jackson parser to lowercase all the names of the keys? I tried using a Jackson PropertyNamingStrategy, but that didn't work - it only seems to be useful when it is getting mapped onto some bean, not a Map.
Clarifications:
I do not want to have to precreate beans for the JSON - I only want dynamic Maps
The JSON keys coming in will not be lowercase, but I want all the map keys to be lowercase (see example below)
The JSON is rather large and heavily nested, so regular expression replacements of the incoming JSON or creating a new map manually after the Jackson parsing is not at all desired.
Incoming JSON:
{"CustName":"Jimmy Smith","Result":"foo","CustNo":"1234"}
The Java map would have:
"custname" => "Jimmy Smith"
"result" => "foo"
"custno" => "1234"
[UPDATE]: The answer I gave below doesn't fully solve the problem. Still looking for a solution.
(nb this solution is tested only with Jackson 2)
It's possible to do this by wrapping the JsonParser and simply applying .toLowerCase() to all field names:
private static final class DowncasingParser extends JsonParserDelegate {
private DowncasingParser(JsonParser d) {
super(d);
}
#Override
public String getCurrentName() throws IOException, JsonParseException {
if (hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
return delegate.getCurrentName().toLowerCase();
}
return delegate.getCurrentName();
}
#Override
public String getText() throws IOException, JsonParseException {
if (hasTokenId(JsonTokenId.ID_FIELD_NAME)) {
return delegate.getText().toLowerCase();
}
return delegate.getText();
}
}
You then have to have a custom JsonFactory to apply your wrapper, as in this test:
#Test
public void downcase_map_keys_by_extending_stream_parser() throws Exception {
#SuppressWarnings("serial")
ObjectMapper mapper = new ObjectMapper(new JsonFactory() {
#Override
protected JsonParser _createParser(byte[] data, int offset, int len, IOContext ctxt) throws IOException {
return new DowncasingParser(super._createParser(data, offset, len, ctxt));
}
#Override
protected JsonParser _createParser(InputStream in, IOContext ctxt) throws IOException {
return new DowncasingParser(super._createParser(in, ctxt));
}
#Override
protected JsonParser _createParser(Reader r, IOContext ctxt) throws IOException {
return new DowncasingParser(super._createParser(r, ctxt));
}
#Override
protected JsonParser _createParser(char[] data, int offset, int len, IOContext ctxt, boolean recyclable)
throws IOException {
return new DowncasingParser(super._createParser(data, offset, len, ctxt, recyclable));
}
});
assertThat(
mapper.reader(Map.class)
.with(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES)
.with(JsonParser.Feature.ALLOW_SINGLE_QUOTES)
.readValue("{CustName:'Jimmy Smith', CustNo:'1234', Details:{PhoneNumber:'555-5555',Result:'foo'} } }"),
equalTo((Map<String, ?>) ImmutableMap.of(
"custname", "Jimmy Smith",
"custno", "1234",
"details", ImmutableMap.of(
"phonenumber", "555-5555",
"result", "foo"
)
)));
}
I figured out one way to do it. Use a org.codehaus.jackson.map.KeyDeserializer, put it in a SimpleModule and register that module with the Jackson ObjectMapper.
import org.codehaus.jackson.map.KeyDeserializer;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.map.module.SimpleModule;
import org.codehaus.jackson.Version;
// ...
class LowerCaseKeyDeserializer extends KeyDeserializer {
#Override
public Object deserializeKey(String key, DeserializationContext ctx)
throws IOException, JsonProcessingException {
return key.toLowerCase();
}
}
// ...
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule("LowerCaseKeyDeserializer",
new Version(1,0,0,null));
module.addKeyDeserializer(Object.class, new LowerCaseKeyDeserializer());
mapper.registerModule(module);
Map<String,Object> map =
(Map<String,Object>) mapper.readValue(jsonStr, Map.class);
[UPDATE]: Actually this only will lowercase the top level map keys, but not nested keys.
If the input is:
{"CustName":"Jimmy Smith","CustNo":"1234","Details":{"PhoneNumber": "555-5555", "Result": "foo"}}
The output in the map, unfortunately, will be:
{"custname"="Jimmy Smith", "custno"="1234", "details"={"PhoneNumber"="555-5555", "Result"="foo"}}
With Jackson there isn't any function that will lower the keys in a nested fashion. Atleast not that I know of. I wrote this simple recursive code that does the job.
public JSONObject recursiveJsonKeyConverterToLower(JSONObject jsonObject) throws JSONException
{
JSONObject resultJsonObject = new JSONObject();
#SuppressWarnings("unchecked") Iterator<String> keys = jsonObject.keys();
while(keys.hasNext())
{
String key = keys.next();
Object value = null;
try
{
JSONObject nestedJsonObject = jsonObject.getJSONObject(key);
value = this.recursiveJsonKeyConverterToLower(nestedJsonObject);
}
catch(JSONException jsonException)
{
value = jsonObject.get(key);
}
resultJsonObject.put(key.toLowerCase(), value);
}
return resultJsonObject;
}
Passed String:
String json = "{'Music': 0, 'Books': {'Biology': 1.1, 'Chemistry': {'Inorganic': true, 'Organic': ['Atom', 'Molecule']}}, 'Food': {'Chicken': [1, 2, 3]}}";
Output:
{"music":0,"books":{"biology":1.1,"chemistry":{"inorganic":true,"organic":["Atom","Molecule"]}},"food":{"chicken":[1,2,3]}}
Its also easy to get Map<String, Object> instead of JSONObject (which is what you want) by making resultJsonObject to be of type Map and other little tweaks.
WARNING: for nested JSON, the result would be of type Map<String, Map<String, Object>> depending on how nested is your json object.
public void setKeyName(String systemName){
this.systemName = systemName.toLowerCase();
}
Below is the second JSON message:
{
"ModeL":"Tesla",
"YeaR":"2015"
}
Normally, default ObjectMapper cannot deserialize this message into a CarInfo object. With following configuration, it’s possible:
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, true);
CarInfo info = objectMapper.readValue(data, CarInfo.class); //'data' contains JSON string
This deserialization is valid. his deserialization is valid.
https://mtyurt.net/post/jackson-case-insensitive-deserialization.html
I am using the Retrofit library for my REST calls. Most of what I have done has been smooth as butter but for some reason I am having issues converting JSON timestamp strings into java.util.Date objects. The JSON that is coming in looks like this.
{
"date": "2013-07-16",
"created_at": "2013-07-16T22:52:36Z",
}
How can I tell Retrofit or Gson to convert these strings into java.util.Date objects?
Gson gson = new GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss")
.create();
RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint(API_BASE_URL)
.setConverter(new GsonConverter.create(gson))
.build();
Or the Kotlin equivalent:
val gson = GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss").create()
RestAdapter restAdapter = Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.create(T::class.java)
You can set your customized Gson parser to retrofit. More here: Retrofit Website
Look at Ondreju's response to see how to implement this in retrofit 2.
#gderaco's answer updated to retrofit 2.0:
Gson gson = new GsonBuilder()
.setDateFormat("yyyy-MM-dd'T'HH:mm:ss")
.create();
Retrofit retrofitAdapter = new Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create(gson))
.build();
Here is how I did it:
Create DateTime class extending Date and then write a custom deserializer:
public class DateTime extends java.util.Date {
public DateTime(long readLong) {
super(readLong);
}
public DateTime(Date date) {
super(date.getTime());
}
}
Now for the deserializer part where we register both Date and DateTime converters:
public static Gson gsonWithDate(){
final GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(Date.class, new JsonDeserializer<Date>() {
final DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
#Override
public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
try {
return df.parse(json.getAsString());
} catch (final java.text.ParseException e) {
e.printStackTrace();
return null;
}
}
});
builder.registerTypeAdapter(DateTime.class, new JsonDeserializer<DateTime>() {
final DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
#Override
public DateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
try {
return new DateTime(df.parse(json.getAsString()));
} catch (final java.text.ParseException e) {
e.printStackTrace();
return null;
}
}
});
return builder.create();
}
And when you create your RestAdapter, do the following:
new RestAdapter.Builder().setConverter(gsonWithDate());
Your Foo should look like this:
class Foo {
Date date;
DateTime created_at;
}
Gson can handle only one datetime format (those specified in builder) plus the iso8601 if parsing with custom format is not possible. So, a solution could be to write your custom deserializer. To solve your problem I defined:
package stackoverflow.questions.q18473011;
import java.util.Date;
public class Foo {
Date date;
Date created_at;
public Foo(Date date, Date created_at){
this.date = date;
this.created_at = created_at;
}
#Override
public String toString() {
return "Foo [date=" + date + ", created_at=" + created_at + "]";
}
}
with this deserializer:
package stackoverflow.questions.q18473011;
import java.lang.reflect.Type;
import java.text.*;
import java.util.Date;
import com.google.gson.*;
public class FooDeserializer implements JsonDeserializer<Foo> {
public Foo deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
String a = json.getAsJsonObject().get("date").getAsString();
String b = json.getAsJsonObject().get("created_at").getAsString();
SimpleDateFormat sdfDate = new SimpleDateFormat("yyyy-MM-dd");
SimpleDateFormat sdfDateWithTime = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
Date date, created;
try {
date = sdfDate.parse(a);
created = sdfDateWithTime.parse(b);
} catch (ParseException e) {
throw new RuntimeException(e);
}
return new Foo(date, created);
}
}
Final step is to create a Gson instance with right adapter:
package stackoverflow.questions.q18473011;
import com.google.gson.*;
public class Question {
/**
* #param args
*/
public static void main(String[] args) {
String s = "{ \"date\": \"2013-07-16\", \"created_at\": \"2013-07-16T22:52:36Z\"}";
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(Foo.class, new FooDeserializer());
Gson gson = builder.create();
Foo myObject = gson.fromJson(s, Foo.class);
System.out.println("Result: "+myObject);
}
}
My result:
Result: Foo [date=Tue Jul 16 00:00:00 CEST 2013, created_at=Tue Jul 16 22:52:36 CEST 2013]
Quite literally if you already have an Date object with the name "created_at" in the class you are creating then it is this easy:
Gson gson = new GsonBuilder().setDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'").create();
YourObject parsedObject1 = gson.fromJson(JsonStringYouGotSomehow, YourObject.class);
And you're done. no complicated overriding needed.
You can define two new classes like this:
import java.util.Date;
public class MyDate extends Date {
}
and
import java.util.Date;
public class CreatedAtDate extends Date {
}
Your POJO will be like this:
import MyDate;
import CreatedAtDate;
public class Foo {
MyDate date;
CreatedAtDate created_at;
}
Finally set your custom deserializer:
public class MyDateDeserializer implements JsonDeserializer<Date> {
public static final SimpleDateFormat sServerDateDateFormat = new SimpleDateFormat("yyyy-MM-dd");
#Override
public MyDate deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
if (json != null) {
final String jsonString = json.getAsString();
try {
return (MyDate) sServerDateDateFormat.parse(jsonString);
} catch (ParseException e) {
e.printStackTrace();
}
}
return null;
}
}
and
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(MyDate.class, new MyDateDeserializer());
This doesn't answer directly the question asked, but is in my opinion the "state of the art" if the coder has the full freedom of choice on how to solve the problem.
First of all, it's not best solution to use java.util.Date. Reason is that those classes had no ideal behaviour in some corner cases so where superseeded by the Java Instant class etc. check the answer of Basil Bourque in this S.O. question: Creating Date objects in Kotlin for API level less than or equal to 16
So I used the Instant class of ThreeTenABP, and
using Kotlin, on Android:
val gson = GsonBuilder().registerTypeAdapter(Instant::class.java,
JsonDeserializer<Instant> { json: JsonElement, _: Type?, _: JsonDeserializationContext? ->
ZonedDateTime.parse(
json.asJsonPrimitive.asString
).toInstant()
}
).create()
val retrofit = Retrofit.Builder()
.baseUrl(baseUrl)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()