15

I'm saving an object with a java.util.Date field into a MongoDB 3.2 instance.

ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(myObject);
collection.insertOne(Document.parse(json));

the String contains:

"captured": 1454549266735

then I read it from the MongoDB instance:

    final Document document = collection.find(eq("key", value)).first();
    final String json = document.toJson();
    ObjectMapper mapper = new ObjectMapper();
    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    xx = mapper.readValue(json, MyClass.class);

the deserialization fails:

java.lang.RuntimeException: com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of java.util.Date out of START_OBJECT token

I see that the json string created by "document.toJson()" contains:

"captured": {
    "$numberLong": "1454550216318"
}

instead of what was there originally ("captured": 1454549266735) MongoDB docs say they started using "MongoDB Extended Json". I tried both Jackson 1 and 2 to parse it - no luck.

what is the easiest way to convert those Document objects provided by MongoDB 3 to Java POJOs? maybe I can skip toJson() step altogether?

I tried mongojack - that one does not support MongoDB3.

Looked at couple other POJO mappers listed on MongoDB docs page - they all require putting their custom annotations to Java classes.

Alex
  • 2,589
  • 3
  • 35
  • 44

4 Answers4

17

You should define and use custom JsonWriterSettings to fine-tune JSON generation:

 JsonWriterSettings settings = JsonWriterSettings.builder()
         .int64Converter((value, writer) -> writer.writeNumber(value.toString()))
         .build();

 String json = new Document("a", 12).append("b", 14L).toJson(settings);

Will produce:

 { "a" : 12, "b" : 14 }

If you will not use custom settings then document will produce extended json:

 { "a" : 12, "b" : { "$numberLong" : "14" } }
caiiiycuk
  • 1,466
  • 14
  • 20
  • Edit: just a simple and a plain Thank You. – 3xCh1_23 Dec 08 '17 at 14:10
  • Perfect answer! Thanks! – ADJ Mar 08 '18 at 09:55
  • 2
    A potential problem with this approach is that large values of longs can lose precision. Some JSON implementations use only doubles. These have a 52 bit mantissa, which is 12 fewer bits than a long. Luckily, epochal time in milliseconds currently only needs 31 bits so we'll get away with this in many cases (for 143,000 years) but beware! – F. P. Freely Sep 10 '18 at 16:47
  • 1
    Now, it is simpler with JsonMode.RELAXED: [spec](https://github.com/mongodb/specifications/blob/master/source/extended-json.rst#relaxed-extended-json-example) – Gibezynu Nu Nov 13 '19 at 11:31
  • How you resolved this - I am getting error - Type Mismatch required Converter[Long] Actual [(Any,Any), Nothing]. Cannot resolve symbol value. – tenderfoot Dec 30 '19 at 08:38
4

This looks like Mongo Java driver bug, where Document.toJson profuces non-standard JSON even if JsonMode.STRICT is used. This problem is described in the following bug https://jira.mongodb.org/browse/JAVA-2173 for which I encourage you to vote.

A workaround is to use com.mongodb.util.JSON.serialize(document).

Aleksey Korolev
  • 367
  • 3
  • 10
1

I save a tag with my mongo document that specifies the original type of the object stored. I then use Gson to parse it with the name of that type. First, to create the stored Document

private static Gson gson = new Gson();

public static Document ConvertToDocument(Object rd) {
    if (rd instanceof Document)
        return (Document)rd;
    String json = gson.toJson(rd);
    Document doc = Document.parse(json); 
    doc.append(TYPE_FIELD, rd.getClass().getName());
    return doc;
}

then to read the document back into the Java,

public static Object ConvertFromDocument(Document doc) throws CAAException {
    String clazzName = doc.getString(TYPE_FIELD);
    if (clazzName == null)
        throw new RuntimeException("Document was not stored in the DB or got stored without becing created by itemToStoredDocument()");
    Class<?> clazz;
    try {
        clazz = (Class<?>) Class.forName(clazzName);
    } catch (ClassNotFoundException e) {
        throw new CAAException("Could not load class " + clazzName, e);
    }

    json = com.mongodb.util.JSON.serialize(doc);
    return gson.fromJson(json, clazz);
}

Thanks to Aleksey for pointing out JSON.serialize().

David Wood
  • 434
  • 1
  • 5
  • 14
  • Isn't this gonna impact performance? I mean, you serialize a document to a string and then the same string is deserialized into the relevant POJO. That sounds very expensive to me... – Romeo Sierra Nov 13 '21 at 02:26
0

It looks like you are using Date object inside "myObject". In that case, you should use a DateSerializer that implements JsonSerializer<LocalDate>, JsonDeserializer<LocalDate> and then register it with GsonBuilder. Sample code follows:

public class My_DateSerializer implements JsonSerializer<LocalDate>,
                                                          JsonDeserializer<LocalDate> {

@Override
public LocalDate deserialize(JsonElement json, Type typeOfT,
                        JsonDeserializationContext context) throws JsonParseException {
    final String dateAsString = json.getAsString();
    final DateTimeFormatter dtf = DateTimeFormat.forPattern(DATE_FORMAT);
    if (dateAsString.length() == 0)
    {
        return null;
    }
    else
    {
        return dtf.parseLocalDate(dateAsString);
    }
}

@Override
public JsonElement serialize(LocalDate src, Type typeOfSrc,
                                                     JsonSerializationContext context) {
    String retVal;
    final DateTimeFormatter dtf = DateTimeFormat.forPattern(DATE_FORMAT);
    if (src == null)
    {
        retVal = "";
    }
    else
    {
        retVal = dtf.print(src);
    }
    return new JsonPrimitive(retVal);
}
}

Now register it with GsonBuilder:

final GsonBuilder builder = new GsonBuilder()
           .registerTypeAdapter(LocalDate.class, new My_DateSerializer());      
final Gson gson = builder.create();
S.R
  • 2,819
  • 2
  • 22
  • 49