I have following class:
public class Some implements Map<String, Object>{
private Map<String, Object> innerMap;
//implementation that can only set innerMap in constructor and cannot add or remove values
}
The problem is that I cannot deserialize this in jackson correctly. If I serialize without default typing, it is OK, since it is serialized as {"one":"two"} and deserialized correctly (I had to implement deserializer with
return new Some(jp.readValueAs(new TypeReference<HashMap<String,Object>>(){}));
When I use default typing turned on, this is serialized as
["com.class.Some",{"one":"two"}]
But deserialization is throwing
com.fasterxml.jackson.databind.JsonMappingException: Unexpected token (START_OBJECT), expected START_ARRAY: need JSON Array to contain As.WRAPPER_ARRAY type information for class java.util.HashMap
Any thoughts?
Annotate your constructor with #JsonCreator:
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
Some some = new Some(new HashMap<String, Object>() {{put("a", 1);}});
String json = mapper.writeValueAsString(some);
System.out.println("serialized : " + json);
some = mapper.readValue(json, Some.class);
System.out.println("deserialized: " + some);
}
// Read only delegating Map
public static class Some extends AbstractMap<String, Object> {
private Map<String, Object> delegate;
#JsonCreator
public Some(Map<String, Object> delegate) {
this.delegate = Collections.unmodifiableMap(delegate);
}
#Override
public Set<Entry<String, Object>> entrySet() {
return delegate.entrySet();
}
}
This is what I needed - custom deserializer:
public class SomeDeserializer extends JsonDeserializer<Some> {
#Override
public Object deserializeWithType(JsonParser jsonParser, DeserializationContext ctxt, TypeDeserializer typeDeserializer) throws IOException {
return typeDeserializer.deserializeTypedFromObject(jsonParser, ctxt);
}
#SuppressWarnings("unchecked")
#Override
public Some deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException {
JsonDeserializer<Object> deserializer = ctxt.findRootValueDeserializer(
ctxt.getTypeFactory().constructMapType(Map.class, String.class, Object.class));
return new Some((Map) deserializer.deserialize(jp, ctxt, new HashMap<>()));
}
}
Related
I need to serialize a graph to JSON containing List and Map. Each map instance contains a UUID field. The graph can contain more than one Map instance with the same UUID. Maps with the same UUID are considered identical.
During Serialization, I would like to replace map instances that have a previously been serialized by only their UUID.
What is the best way to achieve that with Jackson?
Thanks
You can implement a custom serializer for your graph class.
You have to extend StdSerializer and override
#Override
public void serialize(T value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException
When you did that you need to let jackson know about your serializer. You can achieve that by annotate your graph class with #JsonSerialize(using = CustomSerializer.class) or you could register a new module containing the custom serializer.
Below is the working solution I came up with.
However, is there a more elegant way to get a lifecycle hook on top-level serialize calls (which is needed to re-init the custom serializer)?
Also, I'm not convinced that keeping track of visited objects per thread, using ThreadLocal, is the best solution. Any advices?
Thanks
public class IdentifiableSerializerTest {
public static void main(String[] args) throws JsonProcessingException {
ObjectMapper mapper = createObjectMapper();
test(mapper);
}
interface Identifiable {
Long getId();
}
public static ObjectMapper createObjectMapper() {
ObjectMapper mapper = new ObjectMapper();
// disable quoting - for testing purpose
mapper.configure(JsonGenerator.Feature.QUOTE_FIELD_NAMES, false);
mapper.configure(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
// register serializer for Identifiable type
SimpleModule module = new SimpleModule();
module.addSerializer(Identifiable.class, new IdentifiableSerializer(mapper.writer()));
mapper.registerModule(module);
// lifecycle hook to re-init IdentifiableSerializer on root-level serialize calls
mapper.setSerializerProvider(new IdentifiableSerializerProvider());
return mapper;
}
/**
* This class serves to intercept root-level serialize calls in order to
* clean the map of visited objects, see {#link IdentifiableSerializer#visited}.
*
* TODO: this seems lot of code just to get a hook on root-level serialize calls...
*/
public static class IdentifiableSerializerProvider extends DefaultSerializerProvider {
public IdentifiableSerializerProvider() { super(); }
protected IdentifiableSerializerProvider(SerializerProvider src, SerializationConfig config, SerializerFactory f) {
super(src, config, f);
}
#Override
public DefaultSerializerProvider createInstance(SerializationConfig config, SerializerFactory f) {
return new IdentifiableSerializerProvider(this, config, f);
}
#Override
public void serializeValue(JsonGenerator gen, Object value) throws IOException {
IdentifiableSerializer.reset();
super.serializeValue(gen, value);
}
}
public static class IdentifiableSerializer extends JsonSerializer<Identifiable> {
private static ThreadLocal<Set> visited = new ThreadLocal<Set>() {
#Override
protected Set initialValue() {
return new HashSet();
}
};
public static void reset() {
visited.get().clear();
}
private final ObjectWriter delegate;
public IdentifiableSerializer(ObjectWriter delegate) {
this.delegate = delegate;
}
#Override
public void serialize(Identifiable value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
Long id = value.getId();
Set seen = visited.get();
if (seen.contains(id)) {
jgen.writeStartObject();
jgen.writeNumberField("#REF", id);
jgen.writeEndObject();
}
else {
seen.add(id);
delegate.writeValue(jgen, value);
}
}
}
static class IdentifiableMap extends HashMap implements Identifiable {
static long counter = 0;
Long id = counter++;
{
put("#ID", id);
}
#Override
public Long getId() {
return id;
}
}
public static void test(ObjectMapper mapper) throws JsonProcessingException {
Map myMap = new IdentifiableMap() {{
put("key1", 1);
put("key2", 2);
put("key3", 3);
}};
List<Map> myList = Arrays.asList(myMap, myMap);
String expected = "[{key1:1,key2:2,key3:3,#ID:0},{#REF:0}]";
String actual = mapper.writeValueAsString(myList);
Assert.assertEquals(expected, actual);
System.out.println("SUCCESS");
}
}
Given the following POJO, I would like to apply SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED on the links field ONLY.
public class HalRepresentation {
#JsonProperty("_links")
#JsonInclude(JsonInclude.Include.NON_DEFAULT)
private final Map<String, List<Link>> links = new HashMap<String, List<Link>>();
#JsonProperty("_embedded")
#JsonInclude(JsonInclude.Include.NON_DEFAULT)
private final Map<String, Object> embedded = new HashMap<String, Object>();
protected HalRepresentation() {
}
public Map<String, List<Link>> getLinks() {
return links;
}
public Map<String, Object> getEmbedded() {
return embedded;
}
}
I tried to serialize it as follows:
ObjectMapper objectMapper = new ObjectMapper()
.enable(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED);
try {
outputStream.write(objectMapper.writeValueAsBytes(halRepresentation));
outputStream.flush();
} catch (JsonProcessingException e) {
throw new IllegalStateException(e);
}
But when I do this the unwrap feature is also applied on the embedded field. I tried to find an equivalent annotation for WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED, but I can't find one. Do you have an idea for this using Jackson ?
As stated by #AlexeyGavrilov, it does not seem to be possible: https://stackoverflow.com/a/29133209/1225328. A workaround could be to create a custom JsonSerializer:
public class SingleElementCollectionsUnwrapper extends JsonSerializer<Object> {
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
if (!serializers.getConfig().isEnabled(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED)) {
new ObjectMapper().enable(SerializationFeature.WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED).writeValue(gen, value);
} else {
gen.writeObject(value);
}
}
}
Then, annotate the links field with #JsonSerialize:
#JsonProperty("_links")
#JsonInclude(JsonInclude.Include.NON_DEFAULT)
#JsonSerialize(using = SingleElementCollectionsUnwrapper.class)
private final Map<String, List<Link>> links = new HashMap<String, List<Link>>();
I have a class which looks like this:
#JsonFormat(shape=JsonFormat.Shape.OBJECT)
public class MyMap implements Map<String, String>
{
protected Map<String, String> myMap = new HashMap<String, String>();
protected String myProperty = "my property";
public String getMyProperty()
{
return myProperty;
}
public void setMyProperty(String myProperty)
{
this.myProperty = myProperty;
}
//
// java.util.Map mathods implementations
// ...
}
And a main method with this code:
MyMap map = new MyMap();
map.put("str1", "str2");
ObjectMapper mapper = new ObjectMapper();
mapper.getDeserializationConfig().withAnnotationIntrospector(new JacksonAnnotationIntrospector());
mapper.getSerializationConfig().withAnnotationIntrospector(new JacksonAnnotationIntrospector());
System.out.println(mapper.writeValueAsString(map));
When executing this code I'm getting the following output: {"str1":"str2"}
My question is why the internal property "myProperty" is not serialized with the map?
What should be done to serialize internal properties?
Most probably you will end up with implementing your own serializer which will handle your custom Map type. Please refer to this question for more information.
If you choose to replace inheritance with composition, that is to make your class to include a map field not to extend a map, then it is pretty easy to solve this using the #JsonAnyGetter annotation.
Here is an example:
public class JacksonMap {
public static class Bean {
private final String field;
private final Map<String, Object> map;
public Bean(String field, Map<String, Object> map) {
this.field = field;
this.map = map;
}
public String getField() {
return field;
}
#JsonAnyGetter
public Map<String, Object> getMap() {
return map;
}
}
public static void main(String[] args) throws JsonProcessingException {
Bean map = new Bean("value1", Collections.<String, Object>singletonMap("key1", "value2"));
ObjectMapper mapper = new ObjectMapper();
System.out.println(mapper.writeValueAsString(map));
}
}
Output:
{"field":"value1","key1":"value2"}
I have the following class:
class A{
String abc;
String def;
// appropriate getters and setters with JsonProperty Annotation
}
and I call Jacksons objectMapper.writeValueAsString(A) which works fine.
Now I need to add another instance member:
class A{
String abc;
String def;
JSONObject newMember; // No, I cannot Stringify it, it needs to be JSONObject
// appropriate getters and setters with JsonProperty Annotation
}
but when I serialize, I am getting exception:
org.codehaus.jackson.map.JsonMappingException: No serializer found for class org.json.JSONObject and no properties discovered to create BeanSerializer
I tried JSONNode but it gave Output as {outerjson:"{innerjson}"} not {outerjson:{innerjson}}.
Is it possible to use Jackson to achieve the above output, i.e. JSONObject within JSONObject?
What about to use jackson-datatype-json-org
// import com.fasterxml.jackson.datatype.jsonorg.JsonOrgModule;
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JsonOrgModule());
See https://github.com/FasterXML/jackson-datatype-json-org
Well, if you cannot replace the JSONObject on a POJO or a Map, then you can write a custom serializer. Here is an example:
public class JacksonJSONObject {
public static class MyObject {
public final String string;
public final JSONObject object;
#JsonCreator
public MyObject(#JsonProperty("string") String string, #JsonProperty("object") JSONObject object) {
this.string = string;
this.object = object;
}
#Override
public String toString() {
return "MyObject{" +
"string='" + string + '\'' +
", object=" + object +
'}';
}
}
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule("org.json");
module.addSerializer(JSONObject.class, new JsonSerializer<JSONObject>() {
#Override
public void serialize(JSONObject value, JsonGenerator jgen, SerializerProvider provider) throws IOException {
jgen.writeRawValue(value.toString());
}
});
module.addDeserializer(JSONObject.class, new JsonDeserializer<JSONObject>() {
#Override
public JSONObject deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException {
Map<String, Object> bean = jp.readValueAs(new TypeReference<Map<String, Object>>() {});
return new JSONObject(bean);
}
});
mapper.registerModule(module);
JSONObject object = new JSONObject(Collections.singletonMap("key", "value"));
String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(new MyObject("string", object));
System.out.println("JSON: " + json);
System.out.println("Object: " + mapper.readValue(json, MyObject.class));
}
}
Output:
JSON: {
"string" : "string",
"object" : {"key":"value"}
}
Object: MyObject{string='string', object={"key":"value"}}
Use #JsonSerialize with the attribute and implement a custom serializer.
#JsonSerialize(using = JsonObjectSerializer.class)
private JSONObject jsonObject;
public static class JsonObjectSerializer extends JsonSerializer<JSONObject> {
#Override
public void serialize(JSONObject jsonObject, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
jsonGenerator.writeRawValue(jsonObject.toString());
}
}
Use JsonNode instead of JSONObject.
JsonNode jsonNode = JsonLoader.fromString(YOUR_STRING);
I want to serialize a custom Map to JSON.
The class with implements the map interface is the following:
public class MapImpl extends ForwardingMap<String, String> {
//ForwardingMap comes from Guava
private String specialInfo;
private HashMap<String, String> delegate;
#Override
protected Map<String, String> delegate() {
return this.delegate;
}
// some getters....
}
If I call now
ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(new File("/somePath/myJson.json"), objectOfMapImpl);
Jackson will serialize the map and ignores the variable specialInfo
I tried some things with a custom implementation of JsonSerializer but I ended up with this snippet:
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule("someModule");
module.addSerializer(CheapestResponseDates.class, new JsonSerializer<MapImpl>() {
#Override
public void serialize(final MapImpl value, final JsonGenerator jgen, final SerializerProvider provider) throws IOException, JsonProcessingException {
CheapestResponseDurations.class);
// how to serialize the map here? maybe be in a data node...
jgen.writeStartObject();
jgen.writeObjectField("info", value.getInfo());
jgen.writeEndObject();
}
});
mapper.registerModule(module);
I am using JDK 1.7 and Jackson 2.3.1
You can leverage the #JsonAnySetter/ #JsonAnyGetter annotations, as described in this blog post. Since, as you mentioned, your custom map class must implement a Map interface, you could extract a separate "bean" interface and tell Jackson to use it instead when serializing via #JsonSerialize(as = ...) annotation.
I've slightly modified you example to illustrate how it could work. Note that if you want to deserialize the json string back to your map object, you may need to do some other tricks.
public class MapSerialize {
public static interface MyInterface {
String getSpecialInfo();
#JsonAnyGetter
Map<String, String> delegate();
}
#JsonSerialize(as = MyInterface.class)
public static class MyImpl extends ForwardingMap<String, String> implements MyInterface {
private String specialInfo;
private HashMap<String, String> delegate = new HashMap<String, String>();
public Map<String, String> delegate() {
return this.delegate;
}
#Override
public String getSpecialInfo() {
return specialInfo;
}
public void setSpecialInfo(String specialInfo) {
this.specialInfo = specialInfo;
}
#Override
public String put(String key, String value) {
return delegate.put(key, value);
}
}
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
MyImpl objectOfMapImpl = new MyImpl();
objectOfMapImpl.setSpecialInfo("specialInfo");
objectOfMapImpl.put("XXX", "YYY");
String json = mapper.writeValueAsString(objectOfMapImpl);
System.out.println(json);
}
}