Search code examples
javamongodbjacksonjackson2

Deserialize bson long primitive json in jackson


I am using MongoDB as our data store, but we want to use Jackson for serialization/deserialization (the Mongo pojo classes don't handle nearly as many scenarios as Jackson - builders for example).

We have this working using a custom CodecProvider - here's the codec itself:

class JacksonCodec<T> implements Codec<T> {


   private final ObjectMapper objectMapper;
    private final Codec<RawBsonDocument> rawBsonDocumentCodec;
    private final Class<T> type;

    public JacksonCodec(ObjectMapper objectMapper,
                        CodecRegistry codecRegistry,
                        Class<T> type) {
        this.objectMapper = objectMapper;
        this.rawBsonDocumentCodec = codecRegistry.get(RawBsonDocument.class);
        this.type = type;
    }

    @Override
    public T decode(BsonReader reader, DecoderContext decoderContext) {
        try {

            RawBsonDocument document = rawBsonDocumentCodec.decode(reader, decoderContext);
            String json = document.toJson();
            return objectMapper.readValue(json, type);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    @Override
    public void encode(BsonWriter writer, Object value, EncoderContext encoderContext) {
        try {

            String json = objectMapper.writeValueAsString(value);

            rawBsonDocumentCodec.encode(writer, RawBsonDocument.parse(json), encoderContext);

        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    @Override
    public Class<T> getEncoderClass() {
        return this.type;
    }
}

this works fine, until we retrieve a document from Mongo that has a long that is greater than Integer.MAXVALUE. When that happens, deserialization fails with the following message:

Caused by: com.fasterxml.jackson.databind.JsonMappingException: Can not deserialize instance of long out of START_OBJECT token.

Looking at the bson, here's how the Mongo data is coming back to us:

"dateStamp" : { "$numberLong" : "1514334498165" }

so... I'm thinking that I need to register an additional deserializer for Jackson to handle this case (check for a token type of ID_START_OBJECT, parse if it's there, otherwise delegate to the built-in deserializer). I tried registering a simple Long deserializer with the ObjectMapper SimpleModule:

public class BsonLongDeserializer  extends JsonDeserializer<Long>{

    @Override
    public Class<Long> handledType() {
        return Long.class;
    }

    @Override
    public Long deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        if (p.currentTokenId() != JsonTokenId.ID_START_OBJECT){
            // have to figure out how to do this for real if we can get the deserilizer to actually get called
            return ctxt.readValue(p, Long.class);
        }
        return null;
    }
}

and register it:

private static ObjectMapper createMapper(){
    SimpleModule module = new SimpleModule();
    module.addDeserializer(Long.class, new BsonLongDeserializer());

    ObjectMapper mapper = new ObjectMapper()
    .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
    .registerModule(module);

    return mapper;
}

but the BsonLongDeserializer never gets called by Jackson (are primitives handled differently and short-circuit the registered deserializers maybe?).

Jackson version 2.9.3. MongoDB driver version 3.6.

If anyone has any suggestions on angles to attack this, I would appreciate hearing them.

Referenced articles that don't seem to help: MongoDB "NumberLong/$numberLong" issue while converting back to Java Object


Solution

  • I got it working by fixing the Mongo side of things by creating a JsonWriterSettings object to suppress the weird json deserialization. This came from here: converting Document objects in MongoDB 3 to POJOS

    The codec now looks like this:

    class JacksonCodec<T> implements Codec<T> {
        private final ObjectMapper objectMapper;
        private final Codec<BsonDocument> rawBsonDocumentCodec;
        private final Class<T> type;
    
        public JacksonCodec(ObjectMapper objectMapper,
                            CodecRegistry codecRegistry,
                            Class<T> type) {
            this.objectMapper = objectMapper;
            this.rawBsonDocumentCodec = codecRegistry.get(BsonDocument.class);
            this.type = type;
        }
    
        @Override
        public T decode(BsonReader reader, DecoderContext decoderContext) {
            try {
                //https://stackoverflow.com/questions/35209839/converting-document-objects-in-mongodb-3-to-pojos
                JsonWriterSettings settings = JsonWriterSettings.builder().int64Converter((value, writer) -> writer.writeNumber(value.toString())).build();
    
                BsonDocument document = rawBsonDocumentCodec.decode(reader, decoderContext);
                String json = document.toJson(settings);
                return objectMapper.readValue(json, type);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        }
    
        @Override
        public void encode(BsonWriter writer, Object value, EncoderContext encoderContext) {
            try {
    
                String json = objectMapper.writeValueAsString(value);
    
                rawBsonDocumentCodec.encode(writer, RawBsonDocument.parse(json), encoderContext);
    
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        }
    
        @Override
        public Class<T> getEncoderClass() {
            return this.type;
        }
    }