I am currently working on an app in which I need to serialize a
HashMap<Object1, Object2> into JSON and then deserialize from JSON to the same `HashMap'.
I am able to serialize it using the usual mapper and overriding the toString() method for Object1.
public String toString(){
String res = Object1.elem1 + ";" + Object1.elem2;
return res
}
I am then able to serialize and get the expected json (where res is the String I defined before easier not to write it all back).*
{res : Object2JsonRepresentation}
Then I want to deserialize, so I use a custom keyDeserializer :
#XmlElement(name="myMap")
#JsonDeserialize(keyUsing = Object1KeyDeserializer.class)
public HashMap <Object1,Object2> myMap = new HashMap <>();
And the Object1KeyDeserializer:
public class Object1KeyDeserializer extends KeyDeserializer{
#Override
public Object1 deserializeKey(String key, DeserializationContext ctxt) throws IOException, JsonProcessingException {
String[] parts = key.split(";");
System.out.println(key);
Elem elem1 = new Elem(parts[1]);
Elem elem2 = new Elem(parts[2]);
Object1 obj = new Object1(elem1,elem2);
return obj;
}
}
Nonetheless, the keyDeserializer never seems to be called, can you explain me the reason. I'm quite new to JSON and would be glad if answers could be detailed.
Instead of using toString() you can create your own serialization format. If you have non primitive key in Map then you can serialize Map as
[
{
"key": <serialized key>,
"value: <serialized value>
},
....
]
In this case your Serializer and Deserializer will be following:
public class CustomSerializer extends StdSerializer<Map<Object1, Object2>> {
protected CustomSerializer() {
super(Map.class, true);
}
#Override
public void serialize(Map<Object1, Object2> map,
JsonGenerator jsonGenerator,
SerializerProvider serializerProvider) throws IOException{
jsonGenerator.writeStartArray();
for (Map.Entry<Object1,Object2> element: map.entrySet()) {
jsonGenerator.writeStartObject();
jsonGenerator.writeObjectField("key", element.getKey());
jsonGenerator.writeObjectField("value", element.getValue());
jsonGenerator.writeEndObject();
}
jsonGenerator.writeEndArray();
}
}
and
public class CustomDeserializer extends StdDeserializer<Map<Object1, Object2>> {
protected CustomDeserializer() {
super(Map.class);
}
#Override
public Map<Object1, Object2> deserialize(JsonParser jsonParser,
DeserializationContext deserializationContext) throws IOException {
Map<Object1, Object2> result = new HashMap<>();
JsonNode node = jsonParser.getCodec().readTree(jsonParser);
for (JsonNode element : node) {
result.put(
jsonParser.getCodec().treeToValue(element.get("key"), Object1.class),
jsonParser.getCodec().treeToValue(element.get("value"), Object2.class)
);
}
return result;
}
}
So you can create class with your field and another Map (for checking that maps with different types works as usual):
public class MapWrapper {
#JsonSerialize(using = CustomSerializer.class)
#JsonDeserialize(using = CustomDeserializer.class)
private Map<Object1, Object2> map = new HashMap<>();
private Map<String, String> someMap = new HashMap<>();
// default constructor, getters, setters
}
Serialized value can be following:
{
"map": [
{
"key": {
"elem1": "qqq",
"elem2": "rrr"
},
"value": {
"fieldFromValue": "xxx"
}
},
{
"key": {
"elem1": "qqq_two",
"elem2": "rrr_two"
},
"value": {
"fieldFromValue": "yyy"
}
}
],
"someMap": {
"key1": "value1"
}
}
Related
There is a class defined follows:
#Data // lombok
public class MyData {
#Required // my custom annotation
String testValue1;
Integer testValue2;
}
And myData is instantiated like that:
MyData myData = new MyData();
myData.setTestValue1("test1");
myData.setTestValue2(123);
I want to serialize myData as json string as follows:
{
"testValue1": {
"type": "String",
"isRequired": "true",
"value": "test1"
},
"testValue2": {
"type": "Integer",
"isRequired": "false",
"value": "123"
},
}
Is there a good way to create json string?
edit|
I put quotes on json string that to be able to valid.
I want to set key as field name and create additional field information.
set field type on "type" key and
if field has #Required annotation, set true on "isRequired" and
set instantiated field value on "value".
So I played a bit around with Jackson Serialization and came to this result (certainly unfinished and not fully tested, but works with your given object).:
Module to make Spring / Jackson known of the new Serializer.
#JsonComponent
public class TestSerializerModule extends SimpleModule {
#Override
public String getModuleName() {
return TestSerializerModule.class.getSimpleName();
}
#Override
public Version version() {
return new Version(
1,
0,
0,
"",
TestSerializerModule.class.getPackage().getName(),
"TestModule"
);
}
#Override
public void setupModule(SetupContext context) {
context.addBeanSerializerModifier(new BeanSerializerModifier() {
#Override
public JsonSerializer<?> modifySerializer(SerializationConfig config, BeanDescription beanDesc, JsonSerializer<?> serializer) {
if (beanDesc.getBeanClass().equals(MyData.class)) { //Add some smart logic here to identify your objects
return new TestSerializer();
}
return serializer;
}
});
}
}
Then the Serialisier itself:
public class TestSerializer extends StdSerializer<Object> {
protected TestSerializer() {
super(Object.class);
}
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {
ClassIntrospector classIntrospector = provider.getConfig().getClassIntrospector();
BasicBeanDescription beanDescription = (BasicBeanDescription) classIntrospector.forSerialization(provider.getConfig(), provider.constructType(value.getClass()), null);
// Start of the MyValue Object
gen.writeStartObject();
beanDescription.findProperties().forEach(p -> {
// Requiered if Annoation is present
boolean required = p.getField().hasAnnotation(Required.class);
try {
// Write all the wanted fields
gen.writeFieldName(p.getName());
gen.writeStartObject();
gen.writeBooleanField("isRequired", required);
gen.writeStringField("type", p.getField().getRawType().getSimpleName());
gen.writeFieldName("value");
Object value1 = p.getGetter().getValue(value);
// Use existing serializer for the value provider.findValueSerializer(value1.getClass()).serialize(value1, gen, provider);
gen.writeEndObject();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
);
gen.writeEndObject();
}
}
Running this test :
#JsonTest
class TestSerializerTest {
#Autowired
ObjectMapper objectMapper;
#Test
public void testSerializer() throws Exception {
MyData value = new MyData();
value.setTestValue1("test1");
value.setTestValue2(123);
String s = objectMapper.writeValueAsString(value);
System.out.println(s);
}
}
gives me this output:
{"testValue1":{"isRequired":false,"type":"String","value":"test1"},"testValue2":{"isRequired":false,"type":"Integer","value":123}}
Hope that gives you an idea where to start and how to proceed from here!
How to parse following kind of JSON Array using Jackson with preserving order of the content:
{
"1": {
"title": "ABC",
"category": "Video",
},
"2": {
"title": "DEF",
"category": "Audio",
},
"3": {
"title": "XYZ",
"category": "Text",
}
}
One simple solution: rather than deserializing it directly as an array/list, deserialize it to a SortedMap<Integer, Value> and then just call values() on that to get the values in order. A bit messy, since it exposes details of the JSON handling in your model object, but this is the least work to implement.
#Test
public void deserialize_object_keyed_on_numbers_as_sorted_map() throws Exception {
ObjectMapper mapper = new ObjectMapper();
SortedMap<Integer, Value> container = mapper
.reader(new TypeReference<SortedMap<Integer, Value>>() {})
.with(JsonParser.Feature.ALLOW_SINGLE_QUOTES)
.with(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES)
.readValue(
"{ 1: { title: 'ABC', category: 'Video' }, 2: { title: 'DEF', category: 'Video' }, 3: { title: 'XYZ', category: 'Video' } }");
assertThat(container.values(),
contains(new Value("ABC", "Video"), new Value("DEF", "Video"), new Value("XYZ", "Video")));
}
public static final class Value {
public final String title;
public final String category;
#JsonCreator
public Value(#JsonProperty("title") String title, #JsonProperty("category") String category) {
this.title = title;
this.category = category;
}
}
But if you want to just have a Collection<Value> in your model, and hide this detail away, you can create a custom deserializer to do that. Note that you need to implement "contextualisation" for the deserializer: it will need to be aware of what the type of the objects in your collection are. (Although you could hardcode this if you only have one case of it, I guess, but where's the fun in that?)
#Test
public void deserialize_object_keyed_on_numbers_as_ordered_collection() throws Exception {
ObjectMapper mapper = new ObjectMapper();
CollectionContainer container = mapper
.reader(CollectionContainer.class)
.with(JsonParser.Feature.ALLOW_SINGLE_QUOTES)
.with(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES)
.readValue(
"{ values: { 1: { title: 'ABC', category: 'Video' }, 2: { title: 'DEF', category: 'Video' }, 3: { title: 'XYZ', category: 'Video' } } }");
assertThat(
container,
equalTo(new CollectionContainer(ImmutableList.of(new Value("ABC", "Video"), new Value("DEF", "Video"),
new Value("XYZ", "Video")))));
}
public static final class CollectionContainer {
#JsonDeserialize(using = CustomCollectionDeserializer.class)
public final Collection<Value> values;
#JsonCreator
public CollectionContainer(#JsonProperty("values") Collection<Value> values) {
this.values = ImmutableList.copyOf(values);
}
}
(note definitions of hashCode(), equals(x) etc. are all omitted for readability)
And finally here comes the deserializer implementation:
public static final class CustomCollectionDeserializer extends StdDeserializer<Collection<?>> implements
ContextualDeserializer {
private JsonDeserializer<Object> contentDeser;
public CustomCollectionDeserializer() {
super(Collection.class);
}
public CustomCollectionDeserializer(JavaType collectionType, JsonDeserializer<Object> contentDeser) {
super(collectionType);
this.contentDeser = contentDeser;
}
#Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
throws JsonMappingException {
if (!property.getType().isCollectionLikeType()) throw ctxt
.mappingException("Can only be contextualised for collection-like types (was: "
+ property.getType() + ")");
JavaType contentType = property.getType().getContentType();
return new CustomCollectionDeserializer(property.getType(), ctxt.findContextualValueDeserializer(
contentType, property));
}
#Override
public Collection<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException,
JsonProcessingException {
if (contentDeser == null) throw ctxt.mappingException("Need context to produce elements of collection");
SortedMap<Integer, Object> values = new TreeMap<>();
for (JsonToken t = p.nextToken(); t != JsonToken.END_OBJECT; t = p.nextToken()) {
if (t != JsonToken.FIELD_NAME) throw ctxt.wrongTokenException(p, JsonToken.FIELD_NAME,
"Expected index field");
Integer index = Integer.valueOf(p.getText());
p.nextToken();
Object value = contentDeser.deserialize(p, ctxt);
values.put(index, value);
}
return values.values();
}
}
This covers at least this simple case: things like the contents of the collection being polymorphic types may require more handling: see the source of Jackson's own CollectionDeserializer.
Also, you could use UntypedObjectDeserializer as a default instead of choking if no context is given.
Finally, if you want the deserializer to return a List with the indices preserved, you can modify the above and just insert a bit of post-processing of the TreeMap:
int capacity = values.lastKey() + 1;
Object[] objects = new Object[capacity];
values.forEach((key, value) -> objects[key] = value);
return Arrays.asList(objects);
I want to deserialise a JSON payload that represent a field that can be a string (unexpanded) or an object (expanded):
Example payload unexpanded: only a string value is sent, that represents contains the id.
{
"id" : "777424881071",
"category" : "/category/12",
"title" : "ACADEMY DINOSAUR",
"description" : "A Epic Drama of ...",
}
Example payload expanded: the corresponding id is sent:
{
"id" : "777424881071",
"category" : {
"id" : "12",
"name" : "Children"
},
"title" : "ACADEMY DINOSAUR",
"description" : "A Epic Drama of ...",
}
In java this can be represented with a type like this:
public class ExpandableField<T> {
private String id;
private T expandedObject;
public ExpandableField(String id, T expandedObject) {
this.id = id;
this.expandedObject = expandedObject;
}
}
See ExpandableField for a complete example from the stripe java client.
I am wondering how I can write a JsonDeserializer<ExpandableField<?>> for Jackson.
public class ExpandableFieldDeserializer extends JsonDeserializer<ExpandableField<?>> {
#Override
public ExpandableField<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonToken token = p.getCurrentToken();
if (token == JsonToken.VALUE_STRING) {
return new ExpandableField<>(p.getValueAsString(), null);
}
else if (token == JsonToken.START_OBJECT) {
//TODO deserialize object and compute id
return new ExpandableField<>(id, object);
}
return null;
}
}
My current solution is to load the current tree and to hardcode what corresponding class is with a small method: private Class<?> computeType(JsonNode node).
public class ExpandableFieldDeserializer extends JsonDeserializer<ExpandableField<?>> {
#Override
public ExpandableField<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
JsonToken token = p.getCurrentToken();
if (token == JsonToken.VALUE_STRING) {
return new ExpandableField<>(p.getValueAsString(), null);
}
else if (token == JsonToken.START_OBJECT) {
ObjectCodec codec = p.getCodec();
JsonNode node = codec.readTree(p);
String id = null;
if (node.has("id")) {
id = node.asText();
}
Class<?> typeClass = computeType(node);
Object object = codec.treeToValue(node, typeClass);
return new ExpandableField<>(id, object);
}
return null;
}
private Class<?> computeType(JsonNode node) {
//...
}
}
I would prefer something more dynamic.
As comparison, with the gson library, the type is accessible in the method signature:
deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
with ((ParameterizedType) typeOfT).getActualTypeArguments()[0], the context is capable to deserialize the object with context.deserialize(json, t).
See a complete example here: ExpandableFieldDeserializer
How can something similar be made with Jackson?
I want to send a minified version of my JSON by minifying the keys.
The Input JSON string obtained after marshalling my POJO to JSON:
{
"stateTag" : 1,
"contentSize" : 10,
"content" : {
"type" : "string",
"value" : "Sid"
}
}
Desired JSON STRING which I want to send over the network to minimize payload:
{
"st" : 1,
"cs" : 10,
"ct" : {
"ty" : "string",
"val" : "Sid"
}
}
Is there any standard way in java to achieve this ??
PS: My json string can be nested with other objects which too I will have to minify.
EDIT:
I cannot change my POJOs to provide annotations. I have XSD files from which I generate my java classes. So changing anything there is not an option.
You can achieve this in Jackson by using #JsonProperty annotation.
public class Pojo {
#JsonProperty(value = "st")
private long stateTag;
#JsonProperty(value = "cs")
private long contentSize;
#JsonProperty(value = "ct")
private Content content;
//getters setters
}
public class Content {
#JsonProperty(value = "ty")
private String type;
#JsonProperty(value = "val")
private String value;
}
public class App {
public static void main(String... args) throws JsonProcessingException, IOException {
ObjectMapper om = new ObjectMapper();
Pojo myPojo = new Pojo(1, 10, new Content("string", "sid"));
System.out.print(om.writerWithDefaultPrettyPrinter().writeValueAsString(myPojo));
}
Outputs:
{
"st" : 1,
"cs" : 10,
"ct" : {
"ty" : "string",
"val" : "sid"
}
}
SOLUTION 2 (Using Custom Serializer):
This solution is specific to your pojo, it means for every pojo you will need a new serializer.
public class PojoSerializer extends JsonSerializer<Pojo> {
#Override
public void serialize(Pojo pojo, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
/* your pojo */
jgen.writeStartObject();
jgen.writeNumberField("st", pojo.getStateTag());
jgen.writeNumberField("cs", pojo.getContentSize());
/* inner object */
jgen.writeStartObject();
jgen.writeStringField("ty", pojo.getContent().getType());
jgen.writeStringField("val", pojo.getContent().getValue());
jgen.writeEndObject();
jgen.writeEndObject();
}
#Override
public Class<Pojo> handledType() {
return Pojo.class;
}
}
ObjectMapper om = new ObjectMapper();
Pojo myPojo = new Pojo(1, 10, new Content("string", "sid"));
SimpleModule sm = new SimpleModule();
sm.addSerializer(new PojoSerializer());
System.out.print(om.registerModule(sm).writerWithDefaultPrettyPrinter().writeValueAsString(myPojo));
SOLUTION 3 (Using a naming strategy):
This solution is a general solution.
public class CustomNamingStrategy extends PropertyNamingStrategyBase {
#Override
public String translate(String propertyName) {
// find a naming strategy here
return propertyName;
}
}
ObjectMapper om = new ObjectMapper();
Pojo myPojo = new Pojo(1, 10, new Content("string", "sid"));
om.setPropertyNamingStrategy(new CustomNamingStrategy());
System.out.print(om.writerWithDefaultPrettyPrinter().writeValueAsString(myPojo));
Use the annotations...
with gson:
adding #SerializedName("st") over the Class Member will serialize the variable stateTag as "st" : 1, it doesnt matter how deep in the json you are going to nest the objects.
How to parse following kind of JSON Array using Jackson with preserving order of the content:
{
"1": {
"title": "ABC",
"category": "Video",
},
"2": {
"title": "DEF",
"category": "Audio",
},
"3": {
"title": "XYZ",
"category": "Text",
}
}
One simple solution: rather than deserializing it directly as an array/list, deserialize it to a SortedMap<Integer, Value> and then just call values() on that to get the values in order. A bit messy, since it exposes details of the JSON handling in your model object, but this is the least work to implement.
#Test
public void deserialize_object_keyed_on_numbers_as_sorted_map() throws Exception {
ObjectMapper mapper = new ObjectMapper();
SortedMap<Integer, Value> container = mapper
.reader(new TypeReference<SortedMap<Integer, Value>>() {})
.with(JsonParser.Feature.ALLOW_SINGLE_QUOTES)
.with(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES)
.readValue(
"{ 1: { title: 'ABC', category: 'Video' }, 2: { title: 'DEF', category: 'Video' }, 3: { title: 'XYZ', category: 'Video' } }");
assertThat(container.values(),
contains(new Value("ABC", "Video"), new Value("DEF", "Video"), new Value("XYZ", "Video")));
}
public static final class Value {
public final String title;
public final String category;
#JsonCreator
public Value(#JsonProperty("title") String title, #JsonProperty("category") String category) {
this.title = title;
this.category = category;
}
}
But if you want to just have a Collection<Value> in your model, and hide this detail away, you can create a custom deserializer to do that. Note that you need to implement "contextualisation" for the deserializer: it will need to be aware of what the type of the objects in your collection are. (Although you could hardcode this if you only have one case of it, I guess, but where's the fun in that?)
#Test
public void deserialize_object_keyed_on_numbers_as_ordered_collection() throws Exception {
ObjectMapper mapper = new ObjectMapper();
CollectionContainer container = mapper
.reader(CollectionContainer.class)
.with(JsonParser.Feature.ALLOW_SINGLE_QUOTES)
.with(JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES)
.readValue(
"{ values: { 1: { title: 'ABC', category: 'Video' }, 2: { title: 'DEF', category: 'Video' }, 3: { title: 'XYZ', category: 'Video' } } }");
assertThat(
container,
equalTo(new CollectionContainer(ImmutableList.of(new Value("ABC", "Video"), new Value("DEF", "Video"),
new Value("XYZ", "Video")))));
}
public static final class CollectionContainer {
#JsonDeserialize(using = CustomCollectionDeserializer.class)
public final Collection<Value> values;
#JsonCreator
public CollectionContainer(#JsonProperty("values") Collection<Value> values) {
this.values = ImmutableList.copyOf(values);
}
}
(note definitions of hashCode(), equals(x) etc. are all omitted for readability)
And finally here comes the deserializer implementation:
public static final class CustomCollectionDeserializer extends StdDeserializer<Collection<?>> implements
ContextualDeserializer {
private JsonDeserializer<Object> contentDeser;
public CustomCollectionDeserializer() {
super(Collection.class);
}
public CustomCollectionDeserializer(JavaType collectionType, JsonDeserializer<Object> contentDeser) {
super(collectionType);
this.contentDeser = contentDeser;
}
#Override
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property)
throws JsonMappingException {
if (!property.getType().isCollectionLikeType()) throw ctxt
.mappingException("Can only be contextualised for collection-like types (was: "
+ property.getType() + ")");
JavaType contentType = property.getType().getContentType();
return new CustomCollectionDeserializer(property.getType(), ctxt.findContextualValueDeserializer(
contentType, property));
}
#Override
public Collection<?> deserialize(JsonParser p, DeserializationContext ctxt) throws IOException,
JsonProcessingException {
if (contentDeser == null) throw ctxt.mappingException("Need context to produce elements of collection");
SortedMap<Integer, Object> values = new TreeMap<>();
for (JsonToken t = p.nextToken(); t != JsonToken.END_OBJECT; t = p.nextToken()) {
if (t != JsonToken.FIELD_NAME) throw ctxt.wrongTokenException(p, JsonToken.FIELD_NAME,
"Expected index field");
Integer index = Integer.valueOf(p.getText());
p.nextToken();
Object value = contentDeser.deserialize(p, ctxt);
values.put(index, value);
}
return values.values();
}
}
This covers at least this simple case: things like the contents of the collection being polymorphic types may require more handling: see the source of Jackson's own CollectionDeserializer.
Also, you could use UntypedObjectDeserializer as a default instead of choking if no context is given.
Finally, if you want the deserializer to return a List with the indices preserved, you can modify the above and just insert a bit of post-processing of the TreeMap:
int capacity = values.lastKey() + 1;
Object[] objects = new Object[capacity];
values.forEach((key, value) -> objects[key] = value);
return Arrays.asList(objects);