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 {
...
}
Related
I have a simple class as property of mage:
// getter/setter omitted for brevity
public class Magic() {
String Spell;
int strength;
}
public class Mage() {
String name;
Magic magic;
}
I need to deserialize JSON from 2 different source strings:
{
"name" : "Sauron",
"magic" : {
"spell" : "Tamador",
"strenght" : 10
}
}
and
{
"name" : "Gandalf",
"magic" : "You shall not pass"
}
or even "You shall not pass" -> Magic object
I thought going with #JsonDeserialize(using = MagicDeserializer.class) would be the way to go with Jackson, but the Parser barfs with "Unrecognized token". Is there a way I can intercept the loading to do my own parsing?
The idea of a custom deserializer is correct, you can extends the StdDeserializer class and in its deserialize method convert the json to a JsonNode separating the two Stringand Object distinct values associated to the magic key in the json:
public class MagicDeserializer extends StdDeserializer<Magic> {
public MagicDeserializer() {
super(Magic.class);
}
#Override
public Magic deserialize(JsonParser jp, DeserializationContext dc) throws IOException, JsonProcessingException {
final ObjectCodec codec = jp.getCodec();
JsonNode root = codec.readTree(jp);
Magic magic = new Magic();
if (root.isTextual()) { //<- magic is a string
magic.setSpell(root.textValue());
return magic;
}
//ok, so magic is an Magic object
return codec.treeToValue(root, Magic.class);
}
}
Then if you annotate your Magic field you can deserialize both the jsons:
#Data
public class Mage {
private String name;
#JsonDeserialize(using = MagicDeserializer.class)
private Magic magic;
}
#Data
public class Magic {
private String Spell;
private int strength;
}
Mage sauron = mapper.readValue(json1, Mage.class);
System.out.println(mapper.writeValueAsString(sauron));
Mage gandalf = mapper.readValue(json2, Mage.class);
System.out.println(mapper.writeValueAsString(gandalf));
I'm new to Spring-boot and am trying to deserialize json array into java String using Jackson in a Spring-boot Application. Something like
{"history": ["historyA", "historyB"]} (JSON Request Body) -> String history;
However, the following error message got logged.
Cannot deserialize instance of `java.lang.String` out of START_ARRAY token
My Controller is similar to
#RestController
public class PatientController {
#PostMapping
public void create(#RequestBody #Valid Patient patient) {
mapper.create(patient);
}
}
My POJO is similar to:
#Data
#NoArgsConstructor
public class Patient {
#JsonDeserialize(contentUsing = PatientHistoryDeserializer.class, contentAs = List.class)
private String history;
My Json Deserializer is similar to:
public class PatientHistoryDeserializer extends JsonDeserializer<String> {
#Override
public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
List<String> histories = new LinkedList<>();
if (p.getCurrentToken() == JsonToken.START_ARRAY) {
while (p.getCurrentToken() != JsonToken.END_ARRAY) {
String history = p.getValueAsString();
if(history.contains("#"))
throw new ClientError(HttpServletResponse.SC_BAD_REQUEST, "invalid...");
histories.add(history);
}
}
return String.join("#", histories);
}
}
Is my goal achievable ? Or any suggestions on how to convert as I wanted ?
This can be done like this
public class PatientHistoryDeserializer extends JsonDeserializer<String> {
#Override
public String deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException {
if (jsonParser.currentToken() == JsonToken.START_ARRAY) {
List<String> histories = new ArrayList<>();
jsonParser.nextToken();
while (jsonParser.hasCurrentToken() && jsonParser.currentToken() != JsonToken.END_ARRAY) {
histories.add(jsonParser.getValueAsString());
jsonParser.nextToken();
}
return String.join("#", histories);
}
return null;
}
}
And usage would like
#JsonDeserialize(using = PatientHistoryDeserializer.class)
String histories;
The purpose of contentUsing and contentAs are a bit different than the use case here. let's take the following example.
class Histories {
Map<String, String> content;
}
and JSON is something like this
{"content": { "key" : ["A","B"]}}
and you want to deserialize this into a map having (key = "A#B")
there are two ways to do it, write custom deserializer or use contentUsing attribute to specify how your values should be deserialized
#JsonDeserialize(contentUsing = PatientHistoryDeserializer.class)
Map<String, String> content;
Similarly, you can use other annotation attributes like keyUsing for keys for maps.
I have a JSON object like
{
"id" : "1",
"children" : ["2","3"]
}
And I have a Java object like (constructor, getters and setters are omitted):
public class Entity {
public String id;
public String children;
}
I want this JSON to be deserialized to my Java object by this code using Jackson:
Entity entity = mapper.readValue(json, Entity.class);
But get the following error:
Can not deserialize instance of java.lang.String out of START_ARRAY token
How can I solve it without changing type of children field?
The children field is expected to have the following value: ["2","3"].
Creating a custom deserializer
Create a custom deserializer to get the raw JSON value. You can choose one of the following implementations, according to your needs:
It will give you the JSON as is, that is, keeping all the spaces and tabs:
public class RawJsonDeserializer extends JsonDeserializer<String> {
#Override
public String deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException {
long begin = jp.getCurrentLocation().getCharOffset();
jp.skipChildren();
long end = jp.getCurrentLocation().getCharOffset();
String json = jp.getCurrentLocation().getSourceRef().toString();
return json.substring((int) begin - 1, (int) end);
}
}
It will give you the JSON without extra spaces and tabs:
public class RawJsonDeserializer extends JsonDeserializer<String> {
#Override
public String deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException {
JsonNode node = jp.getCodec().readTree(jp);
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
return mapper.writeValueAsString(node);
}
}
Annotate your class to use the deserializer defined above
Change the Entity class by annotating the children attribute with #JsonDeserialize referencing the deserializer defined above:
public class Entity {
public String id;
#JsonDeserialize(using = RawJsonDeserializer.class)
public String children;
}
Parsing the JSON
Then parse the JSON using ObjectMapper and Jackson will use your custom deserializer:
String json = "{\"id\":\"1\",\"children\":[\"2\",\"3\"]}";
ObjectMapper mapper = new ObjectMapper();
Entity entity = mapper.readValue(json, Entity.class);
The value of the children attribute will be ["2","3"].
For more details, have a look at this question.
Marshall your objects into JSON format.
Then Unmarshall from the JSON file
public interface MarshallingSupport {
public String marshal(Object object);
public <T> T unmarshal(String s, Class<T> t);
}
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.logging.Level;
import java.util.logging.Logger;
public class JacksonJSONMarshallingSupport implements MarshallingSupport {
private final ObjectMapper mapper;
public JacksonJSONMarshallingSupport(ObjectMapper mapper) {
this.mapper = mapper;
this.mapper.getFactory().configure(JsonGenerator.Feature.ESCAPE_NON_ASCII, true);
}
#Override
public String marshal(Object object) {
try {
return mapper.writeValueAsString(object);
} catch (JsonProcessingException ex) {
throw new RuntimeException(ex);
}
}
#Override
public <T> T unmarshal(String s, Class<T> t) {
try {
T newObj = mapper.readValue(s, t);
return newObj;
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
}
Taking the #Cassio's answer and if you don't want to or you can't annotate your Entity class, just add some configurations.
First create an abstract class [for method annotation purpose you can create an interface, but in this case we will annotate a bean property so we create an abstract class, and if you also want to annotate a method in this abstract class you have to declare that method as abstract] that will be like a mime bean for Jackson configurations:
public abstract class EntityMixIn {
#JsonDeserialize(using = RawJsonDeserializer.class)
public String children;
}
Now, you have to tell your mapper to take this mixin class and act like the original Entity class just for this configuration purpose:
mapper.addMixIn(Entity.class, EntityMixIn.class);
I've a Model object Group
public class Group {
String title;
List<User> members;
String createdBy;
}
I'm using Jackson to serialize this Object. Instead of serializing the whole User object in list "members" I want to serializer just the user.getTitle() field.
Basically I want a HashMap to be something like
{
"title" : "sometitle"
"members" : [user1.getTitle(), user2.getTitle()]
}
I've written a custom serializer for this
public class GroupSerializer extends JsonSerializer<Circle> {
#Override
public void serialize(Group value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
if(value != null) {
gen.writeStartObject();
gen.writeStringField("title", value.getTitle());
gen.writeStringField("createdBy", value.getCreatedBy());
gen.writeFieldName("members");
gen.writeStartArray();
for(User user : value.getMembers()) {
gen.writeString(user.getEmail());
}
gen.writeEndArray();
gen.writeEndObject()
}
}
}
But it's not working. How do I serialize only a field of List instead of whole User Object?
I suggest that you look into Jackson's Converter interface, which seems more suited to the task than creating a custom serializer.
One approach it to create a Converter instance and add it to the ObjectMapper, so that it will be used for the serialization of all User instances.
public class UserConverter extends StdConverter<User, String> {
#Override
public String convert(User user) {
return user.getTitle();
}
}
Register it on your ObjectMapper like this:
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(User.class, new StdDelegatingSerializer(new UserConverter()));
ObjectMapper om = new ObjectMapper().registerModule(simpleModule);
Another approach, in case you don't want to convert all User instances to String, is to annotate selected properties with a converter like this:
public class Group {
String title;
#JsonSerialize(converter = ListUserConverter.class)
List<User> members;
String createdBy;
}
And have a corresponding converter that looks something like this:
public class ListUserConverter extends StdConverter<List<User>, List<String>> {
#Override
public List<String> convert(List<User> users) {
return users.stream().map(User::getTitle).collect(Collectors.toList());
}
}
Try like below :
Group:
#JsonIgnoreProperties(ignoreUnknown=true)
public class Group {
#JsonSerialize(using= TitleSerializer.class)
List<User> members;
//getters and setters
}
User:
public class User {
private String title;
//getters and setters
}
Custom Serializer :
public class TitleSerializer extends StdSerializer<List<User>> {
private static List<User> users=new ArrayList<User>();
protected TitleSerializer(Class<List<User>> t) {
super(t);
// TODO Auto-generated constructor stub
}
#SuppressWarnings("unchecked")
public TitleSerializer(){
this((Class<List<User>>) users.getClass());
}
#Override
public void serialize(List<User> users, JsonGenerator paramJsonGenerator,
SerializerProvider paramSerializerProvider) throws IOException {
paramJsonGenerator.writeStartObject();
List<String> titles=new ArrayList<String>(users.size());
for(User user: users){
titles.add(user.getTitle());
}
paramJsonGenerator.writeObjectField("members", titles);
paramJsonGenerator.writeEndObject();
}
}
Test :
Group group=new Group(Arrays.asList(new User("a"),new User("b"),new User("c")));
ObjectMapper mapper = new ObjectMapper();
String serialized = mapper.writeValueAsString(group);
System.out.println("output "+serialized);
Output:
{"members":["a","b","c"]}
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