Spring deserialize object with JSONObject field - java

I'm having problems to de-serialize an object containing a JSON field in spring. I have a DataTable data structure that has both String fields and a field that should contain a JSONObject:
public class DataTable {
private String identifier;
private String version;
private JSONObject content;
//getters and setters
}
I want to be able to persist this object and retrieve it later on.
When I post such a DataTable object to my Controller and try to retrieve it later, my content field will be empty, regardless of the contents I posted:
{
"identifier": "id1",
"version": "0.0.1",
"content": {
"empty": true
}
}
It seems that jackson is not able to correctly de-serialize the JSONObject type field and will just leave it empty instead. How can I make it deserialize the field correctly?

Jackson has the jackson-datatype-json-org module, which allows us to combine json.org objects with POJOs or deserialize to json.org objects as the top level object.
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-json-org</artifactId>
<version>${jackson2.version}</version>
</dependency>
Here is a test (don't forget to register the JsonOrgModule).
public class JsonOrgJacksonTest {
private final String json =
"{" +
" \"id\": \"one\"," +
" \"content\": {" +
" \"foo\": \"bar\"," +
" \"baz\": \"blah\"" +
" }" +
"}";
#Test
public void testJsonOrg() throws Exception {
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JsonOrgModule());
Model model = mapper.readValue(json, Model.class);
assertThat(model.getId()).isEqualTo("one");
assertThat(model.getContent().get("foo")).isEqualTo("bar");
assertThat(model.getContent().get("baz")).isEqualTo("blah");
}
public static class Model {
private String id;
private JSONObject content;
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public JSONObject getContent() { return content; }
public void setContent(JSONObject content) { this.content = content; }
}
}

JSONObject is not supposed to be serialized. It is for an internal use. If you want to have a JSON representation of this data structure without a model (dedidated class) try converting it into a Map.
Map<String,Object> result =
new ObjectMapper().readValue(dataTable.getContent(), HashMap.class);

You can use a custom deserializer for this:
public class Test {
public static void main(String[] args) throws JsonParseException, JsonMappingException, IOException {
ObjectMapper mapper = new ObjectMapper();
final SimpleModule module = new SimpleModule("configModule", Version.unknownVersion());
module.addDeserializer(DataTable.class, new DeSerializer());
mapper.registerModule(module);
// DataTable readValue = mapper.readValue(<xml source>);
}
}
class DeSerializer extends StdDeserializer<DataTable> {
protected DeSerializer() {
super(DataTable.class);
}
#Override
public DataTable deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
// use p.getText() and p.nextToken to navigate through the xml and construct DataTable object
return new DataTable();
}
}

Related

Ignore fields only in XML but not json in spring boot (xml Mapper)

How to ignore some fields while converting POJO to XML using XMLMapper but not in JSON.
public String getXmlInString(String rootName, Object debtReport) {
XmlMapper xmlMapper = new XmlMapper();
return xmlMapper.writer().withRootName(rootName).withDefaultPrettyPrinter().writeValueAsString(debtReport);
}
POJO Class
Class Employee {
Long id;
String name;
LocalDate dob;
}
The expected output in JSON
{
"id": 1,
"name": "Thirumal",
"dob": "02-04-1991"
}
The expected output in XML (Need to ignore ID)
<Employee>
<name>Thirumal</name>
<dob>02-04-1991</dob>
</Employee>
You can achieve that using JsonView
First declare Views class with two "profiles" - default (only Default fields are serialized) and json-only (both Default and Json fields are serialized):
public class Views {
public static class Json extends Default {
}
public static class Default {
}
}
Then mark always visible fields with Default-view and ID field with Json view:
public class Employee {
#JsonView(Views.Json.class)
Long id;
#JsonView(Views.Default.class)
String name;
#JsonView(Views.Default.class)
String dob;
}
Then instruct mapper to respect given appropriate view during serialization:
#Test
public void test() throws JsonProcessingException {
Employee emp = new Employee();
emp.id = 1L;
emp.name = "John Doe";
emp.dob = "1994-03-02";
// JSON with ID
String json = new ObjectMapper()
.writerWithView(Views.Json.class)
.writeValueAsString(emp);
System.out.println("JSON: " + json);
// XML without ID
String xml = new XmlMapper()
.writerWithView(Views.Default.class)
.writeValueAsString(emp);
System.out.println("XML: " + xml);
}
Finally the output is:
JSON: {"id":1,"name":"John Doe","dob":"1994-03-02"}
XML: <Employee><name>John Doe</name><dob>1994-03-02</dob></Employee>

Automatic deserialization of String to Object with Jackson

Context
Say you have:
public class Dto {
private String name;
private String List<Custom> customs;
// getters and setters...
}
and
public class Custom {
private String something;
private String else;
// getters and setters...
}
Your Spring MVC RestController receives a list of Dto:
#PostMapping
public String create(#RequestBody #Valid List<Dto> dtos) {
return myService.process(features);
}
Input
However, you know that the client-side service which will send data to your controller will send something like this:
[
{
"name": "Bob",
"customs": [
"{\n \"something\": \"yes\",\n \"else\": \"no\"\n }"
]
}
]
Notice how the List<Custom> actually ends up being received as a List<String>. Please assume this cannot be changed on the client-side and we have to deal with it on the server-side.
Question
Is there a Jackson annotation which would automagically take the input String and try to serialize it into a Custom class?
Attempts
A few things that didn't work, including:
#JsonSerialize(using = ToStringSerializer.class)
private List<Custom> customs;
along with
public Custom(String json) {
try {
new ObjectMapper().readerFor(Custom.class).readValue(json);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}
As it is, we have had to change the customs type to List<String> and add a utility method which converts a String into a Custom using an ObjectMapper. This is rather dissatisfying.
You need to implement custom deserialiser or converter which would be used to convert given payload to required type. One trick, you could use is to create new ObjectMapper and use it for internal deserialisation.
Example usage:
class CustomConverter extends StdConverter<String, Custom> {
private final ObjectMapper mapper = new ObjectMapper();
#Override
public Custom convert(String value) {
try {
return mapper.readValue(value, Custom.class);
} catch (JsonProcessingException e) {
throw new IllegalArgumentException(value);
}
}
}
class Dto {
private String name;
#JsonDeserialize(contentConverter = CustomConverter.class)
private List<Custom> customs;
}
You need to create a custom Deserializer.
public class CustomDeserializer extends StdDeserializer<Custom> {
public CustomDeserializer() {
this(null);
}
public CustomDeserializer(Class<?> vc) {
super(vc);
}
#Override
public Custom deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
int id = (Integer) ((IntNode) node.get("id")).numberValue();
String name = node.get("name").asText();
...
return new Custom(id, name, ...);
}
}
and register the deserializer on the Custom class:
#JsonDeserialize(using = CustomDeserializer.class)
public class Custom {
...
}

Java JSON Processing

I'm having a hard time processing the below JSON with Java, which is being returned from on an external Ansible playbook:
{"Sample":
{
"tag_description":"abc","tag_category_id":"def","tag_id":"ghi"
},
"Sample1":
{
"tag_description":"jkl","tag_category_id":"mno","tag_id":"pqr"
}
}
I've been able to successfully parse one section of the JSON using a custom deserializer, though it only ever gets the first section. Any ideas are hugely appreciated.
#JsonComponent
public class TagSerializer extends JsonDeserializer<Tag> {
#Override
public Tag deserialize(JsonParser jsonParser,
DeserializationContext deserializationContext) throws IOException,
JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
JsonFactory factory = mapper.getFactory();
JsonNode treeNode = jsonParser.getCodec().readTree(jsonParser);
Iterator<Map.Entry<String, JsonNode>> fields = treeNode.fields();
String name = "";
// collect the tag name
Map.Entry<String, JsonNode> entry = fields.next();
name = entry.getKey();
// now that we have the tag name, parse it as a separate JSON object
JsonNode node = entry.getValue();
// get the values from the JSON
String description = node.get("tag_description").asText();
String category_id = node.get("tag_category_id").asText();
String tag_id = node.get("tag_id").asText();
return new Tag(name, category_id, description, tag_id);
}
}
I'm calling the method from a Spring Boot REST API endpoint, and my 'tag' model is a Spring entity:
'Tag' model:
#Entity
#JsonDeserialize(using = TagSerializer.class)
public class Tag {
#Id
#GeneratedValue(strategy=GenerationType.AUTO)
private Long id;
private String name;
private String tag_category_id;
private String tag_description;
private String tag_id;
//JPA requires that a default constructor exists
//for entities
protected Tag() {}
public Tag(String name,
String tag_category_id,
String tag_description,
String tag_id) {
this.name = name;
this.tag_category_id = tag_category_id;
this.tag_description = tag_description;
this.tag_id = tag_id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getTag_category_id() {
return tag_category_id;
}
public void setTag_category_id(String tag_category_id) {
this.tag_category_id = tag_category_id;
}
public String getTag_description() {
return tag_description;
}
public void setTag_description(String tag_description) {
this.tag_description = tag_description;
}
public String getTag_id() {
return tag_id;
}
public void setTag_id(String tag_id) {
this.tag_id = tag_id;
}
public String toString() {
return "<Tag:[Name: " + this.name + "],[tag_category: "+
this.tag_category_id + "],[tag_description: "+
this.tag_description + "],[tag_id:"+this.tag_id+"]";
}
}
Spring Boot endpoint:
#PostMapping(value="/store", consumes = APPLICATION_JSON_VALUE)
public void tagJson(#RequestBody String json) {
// delete any existing tags
tagRepository.deleteAll();
//lets modify the json to make it look nicer
String modjson = "["+json+"]";
ObjectMapper mapper = new ObjectMapper();
try {
Tag[] tags = mapper.readValue(modjson, Tag[].class);
for (Tag t : tags)
tagRepository.save(t);
} catch (Exception exception) {
exception.printStackTrace();
}
}
If you are using Spring MVC consider explicitly declare desired type when referreing to #RequestBody and let the framework do the job for you
#PostMapping(value="/store", consumes = APPLICATION_JSON_VALUE)
public void tagJson(#RequestBody Map<String, Tag> json) {
// Do not mess with ObjectMapper here, Spring will do the thing for you
}
This isn't a direct answer but a guide in a possible direction, using Gson.
package test;
import java.util.Map;
import com.google.gson.Gson;
public class JsonTest {
public static void main(final String... args) {
new JsonTest().run();
}
public void run() {
final Gson gson = new Gson();
final Map<?, ?> result = gson.fromJson("{" +
" \"Sample\": {" +
" \"tag_description\": \"abc\"," +
" \"tag_category_id\": \"def\"," +
" \"tag_id\": \"ghi\"" +
" }," +
" \"Sample1\": {" +
" \"tag_description\": \"jkl\"," +
" \"tag_category_id\": \"mno\"," +
" \"tag_id\": \"pqr\"" +
" }" +
"}", Map.class);
System.out.println("Map size: " + result.size());
}
}
The resulting size is 2. The map entries are keyed Sample, Sample1, and the values are lists containing the nodes. You can see this using a debugger.

Jackson: Keep references to keys in map values when deserializing

I have the following JSON with a map from user IDs to user details:
{
"users": {
"john": { "firstName": "John", "lastName": "Doe" },
"mark": { "firstName": "Mark", "lastName": "Smith" }
}
}
and I'm using the following code to deserialize the JSON into a Java objects:
class User {
public String userID;
public String firstName;
public String lastName;
}
public class Users {
public Map<String, User> users;
public static void main(String[] args) throws IOException {
ObjectMapper mapper = new ObjectMapper();
Reader source = Files.newBufferedReader(Paths.get("test.json"));
Users all = mapper.readValue(source, Users.class);
// ...
}
}
After the deserialization, I want the field User.userID to be set to the corresponding key in the users map.
For example all.users.get("john").userID should be "john".
How can I do that?
Create a custom deserializer for User object and use this for the Map. Here's a full example:
#Test
public void test() throws JsonParseException, JsonMappingException, IOException {
ObjectMapper mapper = new ObjectMapper();
Data data = mapper.readValue("{\"users\": {\"John\": {\"id\": 20}, \"Pete\": {\"id\": 30}}}", Data.class);
assertEquals(20, data.users.get("John").id);
assertEquals(30, data.users.get("Pete").id);
assertEquals("John", data.users.get("John").name);
assertEquals("Pete", data.users.get("Pete").name);
}
public static class Data {
#JsonDeserialize(contentUsing = Deser.class)
public Map<String, User> users;
}
public static class User {
public String name;
public int id;
}
public static class Deser extends JsonDeserializer<User> {
#Override
public User deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
String name = ctxt.getParser().getCurrentName();
User user = p.readValueAs(User.class);
user.name = name; // This copies the key name to the user object
return user;
}
}
The simplest solution for the problem is to implement a custom deserializer for the class in which you need the map key (see john16384's answer). This is however cumbersome if you have multiple maps with different value types in your JSON because you'd need one deserializer per type.
In this case, there is a better solution: I would create a custom #JsonMapKey annotation to mark the target properties for the map keys, and then register a generic custom deserializer that processes all occurrences of the annotation. These are the parts you need for this:
Custom #JsonMapKey annotation:
/**
* Annotation used to indicate that the annotated property shall be deserialized to the map key of
* the current object. Requires that the object is a deserialized map value.
*
* Note: This annotation is not a standard Jackson annotation. It will only work if this is
* explicitly enabled in the {#link ObjectMapper}.
*/
#Target({ ElementType.FIELD })
#Retention(RetentionPolicy.RUNTIME)
public #interface JsonMapKey {
}
Custom deserializer that processes the #JsonMapKey annotations:
public class JsonMapKeyDeserializer extends DelegatingDeserializer {
private static final long serialVersionUID = 1L;
private BeanDescription beanDescription;
public JsonMapKeyDeserializer(JsonDeserializer<?> delegate, BeanDescription beanDescription) {
super(delegate);
this.beanDescription = beanDescription;
}
#Override
protected JsonDeserializer<?> newDelegatingInstance(JsonDeserializer<?> newDelegatee) {
return new JsonMapKeyDeserializer(newDelegatee, beanDescription);
}
#Override
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
String mapKey = p.getCurrentName();
Object deserializedObject = super.deserialize(p, ctxt);
// set map key on all fields annotated with #JsonMapKey
for (BeanPropertyDefinition beanProperty : beanDescription.findProperties()) {
AnnotatedField field = beanProperty.getField();
if (field != null && field.getAnnotation(JsonMapKey.class) != null) {
field.setValue(deserializedObject, mapKey);
}
}
return deserializedObject;
}
}
Registration of the custom deserializer in the ObjectMapper:
private static void registerJsonMapKeyAnnotation(ObjectMapper objectMapper) {
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config,
BeanDescription beanDescription, JsonDeserializer<?> originalDeserializer) {
return new JsonMapKeyDeserializer(originalDeserializer, beanDescription);
}
});
objectMapper.registerModule(module);
}
Then you only need to annotate the field to be used for the map key...
class User {
#JsonMapKey
public String userID;
public String firstName;
public String lastName;
}
... and deserialize your JSON with the prepared ObjectMapper:
Users all = registerJsonMapKeyAnnotation(new ObjectMapper()).readValue(source, Users.class);
First Create the ObjectMapper class object than configure it.
Try following one.
Sample Code
Map<K, V> map;
ObjectMapper mapper = new ObjectMapper();
mapper.configure(Feature.ALLOW_UNQUOTED_FIELD_NAMES, true);
map = mapper.readValue(jsonStr, new TypeReference<Map<K, V>>() {});
than you can get the value using Map.

Jackson cannot deserialize enum as object even if I add customized deserializer

I want to use Jackson JSON to serialize/deserialize a class containing an enum object. My class is:
class Bar {
#JsonProperty("rateType")
#JsonDeserialize(using = ReturnedRateTypeDeserializer.class)
private ReturnedRateType rateType;
public ReturnedRateType getRateType() {
return rateType;
}
public void setRateType(ReturnedRateType rateType) {
this.rateType = rateType;
}
}
The enum class ReturnedRateType is defined as:
#JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum ReturnedRateType {
AA("AA"),
BB("BB"),
CC("CC");
#JsonProperty("value")
private String value;
ReturnedRateType(String value) {
this.value = value;
}
#JsonCreator
public static ReturnedRateType fromValue(final String value) {
if (value != null) {
for (ReturnedRateType type : ReturnedRateType.values()) {
if (value.equalsIgnoreCase(type.value)) {
return type;
}
}
}
return null;
}
}
As you see, I added #JsonFormat annotation to tell Jackson to serialize this enum as POJO, and added #JsonCreator annotation to get a static factory method from given string to enum object. Since Jackson can only serialize but can't deserialize from object representation to enum, I added the following customized deserializer for the enum ReturnedRateType:
public class ReturnedRateTypeDeserializer extends JsonDeserializer<ReturnedRateType> {
#Override
public ReturnedRateType deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
ReturnedRateType type = ReturnedRateType.fromValue(jp.getValueAsString());
if(type != null)
return type;
throw new JsonMappingException("invalid value for ReturnedRateType");
}
}
But when I tested deserialization from a JSON string to enum, I got the error. The JSON string is:
{"rateType": {"value": "AA"}}
My test code is:
#Test
public void RateTypeToEnum() {
String json = "{\"rateType\": {\"value\": \"AA\"}}";
System.out.println(json);
ObjectMapper mapper = new ObjectMapper();
Bar bar = null;
try {
bar = mapper.readValue(json, Bar.class);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(bar.getRateType());
}
I expect to see the output should be AA. But jp.getValueAsString() in my customized deserializer ReturnedRateTypeDeserializer is null during the execution:
ReturnedRateType type = ReturnedRateType.fromValue(jp.getValueAsString()); //jp.getValueAsString() is null here!
Thus it returns error. So what is wrong here?
According to the Jackson 2.5.X documentation on the JsonFormat annotation the Shape.Object does not work for the enum deserialisation:
Enums: Shapes JsonFormat.Shape.STRING and JsonFormat.Shape.NUMBER can
be used to change between numeric (index) and textual (name or
toString()); but it is also possible to use JsonFormat.Shape.OBJECT
to serialize (but not deserialize).
I'd make the JsonCreator static method accept a JsonNode and read the string value from it.
Note that this would work since 2.5.X. In early versions you would need to write a custom deserialiser. Here is an example:
public class JacksonEnumObjectShape {
#JsonFormat(shape = JsonFormat.Shape.OBJECT)
#JsonDeserialize(using = ReturnedRateTypeDeserializer.class)
public enum ReturnedRateType {
AA("AA"),
BB("BB"),
CC("CC");
#JsonProperty("value")
private String value;
ReturnedRateType(String value) {
this.value = value;
}
#JsonCreator
public static ReturnedRateType fromValue(final JsonNode jsonNode) {
for (ReturnedRateType type : ReturnedRateType.values()) {
if (type.value.equals(jsonNode.get("value").asText())) {
return type;
}
}
return null;
}
}
// can be avoided since 2.5
public static class ReturnedRateTypeDeserializer extends JsonDeserializer<ReturnedRateType> {
#Override
public ReturnedRateType deserialize(
final JsonParser jp,
final DeserializationContext ctxt) throws IOException {
final JsonNode jsonNode = jp.readValueAsTree();
return ReturnedRateType.fromValue(jsonNode);
}
}
public static void main(String[] args) throws IOException {
final ObjectMapper mapper = new ObjectMapper();
final String json = mapper.writeValueAsString(ReturnedRateType.AA);
System.out.println(json);
System.out.println(mapper.readValue(json, ReturnedRateType.class));
}
}
Output:
{"value":"AA"}
AA

Categories