5

I have a result set from DB in form of Map<String, Object> that I should return as json from a REST-service. Values in the map could be of various types including PGObject, String, Integer and Date.

And I wrote a custom serializer for org.postgresql.util.PGObject class with type "jsonb", which works fine in List<?>, but not in Map<String, Object>.

public class PgObjectSerializer extends JsonSerializer<PGobject>{

    @Override
    public void serialize(PGobject pgObject, JsonGenerator gen, SerializerProvider serializers) throws IOException {
        switch (pgObject.getType()) {
            case "json":
            case "jsonb":
                gen.writeRawValue(pgObject.getValue());
                break;
            default:
                gen.writeString(pgObject.getValue());
        }
    }
}

Target PGObject looks like this:

PGObject pgo = new PGObject();
pgo.setType("jsonb");
pgo.setValue("[{"id": 6, "name": "Foo"}, {"id": 7, "name": "Bar"}, {"id": 8, "name": "Baz"}]"); map.put("reason", pgo);

When Jackson serializes this PGObject value in a Map<String, Object> then I get json value like:

  "reason": {
    "type": "jsonb",
    "value": "[{\"id\": 6, \"name\": \"Foo\"}, {\"id\": 7, \"name\": \"Bar\"}, {\"id\": 8, \"name\": \"Baz\"}]"
  }

What I need:

  "reason": [
    {
      "id": 6,
      "name": "Foo"
    },
    {
      "id": 7,
      "name": "Bar"
    },
    {
      "id": 8,
      "name": "Baz"
    },
  ],

I've tried adding module to ObjectMapper and custom MapType to ObjectWriter as shown in the answer to Serializing Map<Date, String> with Jackson:

@Service
@Transactional
public class MyClass {
    private final ObjectWriter writer;
    private final MyRepo repository;

    @Autowired
    public MyClass(MyRepo repository) {
        this.repository = repository;

        SimpleModule module = new SimpleModule();
        module.addKeySerializer(PGobject.class, new PgObjectSerializer());

        ObjectMapper mapper = new ObjectMapper();
        JavaType myMapType = mapper.getTypeFactory().
                constructMapType(HashMap.class, String.class, PGobject.class);
        writer = mapper.registerModule(module).writerFor(myMapType);
    }

    ...

    private String toJsonString(Map<String, Object> map) {
        try {
            return writer.writeValueAsString(map);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }
}

but I get serialization error for every non-PGObject object in the map:

[Test worker] ERROR 
com.fasterxml.jackson.databind.JsonMappingException: object is not an instance of declaring class (through reference chain: java.util.HashMap["end_date"]->java.lang.String["type"])
    at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:388)
    at com.fasterxml.jackson.databind.JsonMappingException.wrapWithPath(JsonMappingException.java:348)
    at com.fasterxml.jackson.databind.ser.std.StdSerializer.wrapAndThrow(StdSerializer.java:343)
    at com.fasterxml.jackson.databind.ser.std.BeanSerializerBase.serializeFields(BeanSerializerBase.java:698)
    at com.fasterxml.jackson.databind.ser.BeanSerializer.serialize(BeanSerializer.java:155)
    at com.fasterxml.jackson.databind.ser.std.MapSerializer.serializeFieldsUsing(MapSerializer.java:736)
    at com.fasterxml.jackson.databind.ser.std.MapSerializer.serialize(MapSerializer.java:534)
    at com.fasterxml.jackson.databind.ser.std.MapSerializer.serialize(MapSerializer.java:30)
    at com.fasterxml.jackson.databind.ser.DefaultSerializerProvider.serializeValue(DefaultSerializerProvider.java:416)
    at com.fasterxml.jackson.databind.ObjectWriter$Prefetch.serialize(ObjectWriter.java:1425)
    at com.fasterxml.jackson.databind.ObjectWriter._configAndWriteValue(ObjectWriter.java:1158)
    at com.fasterxml.jackson.databind.ObjectWriter.writeValueAsString(ObjectWriter.java:1031)

How to enable my PGObjectSerializer in Jackson when PGObject is found as a value during serialization of a Map<String, Object>?

Rodion
  • 53
  • 2
  • 5
  • Try to use module.addSerializer() instead of addKeySerializer. I think that key serializer works only for keys. – Oleg Cherednik Sep 19 '17 at 11:34
  • @oleg.cherednik, thanks for pointing this out, that was the reason! I've changed to `addSerializer` and now my code works perfectly fine. – Rodion Sep 19 '17 at 12:03

1 Answers1

1

One work-around could be to add serializer for Object.

Then in serializer itself, you can check whether Object is instanceOf PGobject or not.

and in myMapType you can specify Object.class:

JavaType myMapType = mapper.getTypeFactory().
                constructMapType(HashMap.class, String.class, Object.class);

Serializer would be:

class PgObjectSerializer extends JsonSerializer<Object> {
@Override
public void serialize(Object object, JsonGenerator gen, SerializerProvider serializers) throws IOException {
    if (object instanceof PGobject) {
        PGobject pgObject = (PGobject) object;
        switch (pgObject.getType()) {
        case "json":
        case "jsonb":
            gen.writeRawValue(pgObject.getType());
            break;
        default:
            gen.writeString(pgObject.getType());
        }
    } else {
        ObjectMapper mapper = new ObjectMapper();
        mapper.writeValue(gen, object);
    }
}
GreenGiant
  • 4,930
  • 1
  • 46
  • 76
Sachin Gupta
  • 7,805
  • 4
  • 30
  • 45
  • This workaround will require delegating serialization of all other class types to some other "base" or "generic" serializer. I didn't find such one. – Rodion Sep 19 '17 at 10:41
  • You can use obejct mapper for that thing, see my updated PgObjectSerializer – Sachin Gupta Sep 19 '17 at 10:48
  • Unfortunately, the `else` part leads to exception during String key serialization: `Caused by: com.fasterxml.jackson.core.JsonGenerationException: Can not write a string, expecting field name (context: Object)`. I've tried `serializers.defaultSerializeValue(value, gen)` and `gen.writeString(value.toString())` but getting the same error. – Rodion Sep 19 '17 at 11:19
  • try `mapper.writeValue(gen, object);` I have also updated in answer – Sachin Gupta Sep 19 '17 at 11:28
  • After a small fix for `module` from @oleg.cherednik's comment this workaround works as expected. Thanks! – Rodion Sep 19 '17 at 12:13