I'm trying to custom deserialize incoming json to the field, but #JsonDeserialize is not working wheh I put it above the setter of the field I want to deserialize.
public class MyEntity {
#JsonProperty("identifier")
private String identifier;
#JsonDeserialize(using = IdentifierDeserializer.class)
public void setIdentifier(String identifier)
{
this.identifier = identifier;
}
}
....
public class IdentifierDeserializer
extends JsonDeserializer<String>
{
#Override
public String deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException
{
ObjectCodec oc = jp.getCodec();
JsonNode node = oc.readTree(jp);
final JsonNode entity = node.get("entity");
return entity.get("id").asText();
}
}
Im doing this via _restTemplate httpMessageConverter flow, just standar API calls, and no, it does not throw any exceptions, it just not invoked, because I this this with debug and my breakpoints left untouched.
HttpEntity<T> httpEntity = new HttpEntity<>(entity, httpHeaders());
MappingJackson2HttpMessageConverter jsonConverter = new MappingJackson2HttpMessageConverter();
jsonConverter.setSupportedMediaTypes(Lists.newArrayList(MediaType.APPLICATION_JSON_UTF8));
_restTemplate.getMessageConverters().add(jsonConverter);
return _restTemplate.exchange(url, method, httpEntity, MyEntity.class).getBody();
But #JsonDeserialize does work when I put it above the class. So what's the problem guys? Thanks.
Related
From the API client, I am sending
{
"scheduledDateTime": "27-12-2020 08:55:46",
"subscriptionClosedBy": "30-12-2020 08:55:46",
"presenter":{"first":"Jone Doe","second":"Senior Tech Lead"}
}
JSON object to my following API endpoint.
public ResponseEntity<ApiResponse> saveWorkshop(#RequestBody Workshop workshop)
Workshop class contains a Pair type attribute as follows.
public class Workshop extends PersistObject {
private LocalDateTime scheduledDateTime;
private LocalDateTime subscriptionClosedBy;
private Pair<String, String> presenter;
}
When the request hits the endpoint exception is thrown as follows.
Caused by:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot
construct instance of org.springframework.data.util.Pair
How can I overcome this issue?
I created the following custom deserializer class
public final class PairDeserializer extends JsonDeserializer<Pair<String, String>> {
static private ObjectMapper objectMapper = null;
#Override
public Pair<String, String> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
if (objectMapper == null) {
objectMapper = new ObjectMapper();
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
}
JsonNode treeNode = jsonParser.getCodec().readTree(jsonParser);
String first = objectMapper.treeToValue(treeNode.get("first"), String.class);
String second = objectMapper.treeToValue(treeNode.get("second"), String.class);
return Pair.of(first, second);
}
}
and applied it to the Workshop class as follows
#JsonDeserialize(using = PairDeserializer.class)
private Pair<String, String> presenter;
Then the issue was solved.
In our spring data projects we have "standard" approach to writing our DTOs, where we use lombok's #Value and #Builder for immutability and #JsonDeserialize(builder = SomeClass.SomeClassBuilder.class) for jackson deserialization.
Here is a minimal example:
#RestController
class Controller {
#PostMapping("/post")
void post(#RequestBody PostBody body) {
System.out.println(body);
}
}
#Value
#Builder
#JsonDeserialize(builder = PostBody.PostBodyBuilder.class)
class PostBody {
byte[] id;
ClientData clientData;
#JsonPOJOBuilder(withPrefix = "")
public static class PostBodyBuilder {}
}
#Value
#Builder
#JsonDeserialize(builder = ClientData.ClientDataBuilder.class)
class ClientData {
String something;
Integer somethingElse;
#JsonPOJOBuilder(withPrefix = "")
public static class ClientDataBuilder {}
}
This works as fine, as you'd expect, with a normal JSON payload e.g:
{
"id": "c29tZWlk",
"clientData": {
"something": "somethingValue",
"somethingElse": 1
}
}
However, we have a use-case where the clientData structure is known but, for reasons, is sent as a base64 encoded, stringified JSON blob e.g:
{
"id": "c29tZWlk",
"clientData": "eyJzb21ldGhpbmciOiJzb21ldGhpbmdWYWx1ZSIsInNvbWV0aGluZ0Vsc2UiOjF9"
}
It would be great if we could transparently decode and de-stringify this field as part of the deserialisation of PostBody before it calls runs the deserializer for ClientData.
One solution is create a custom deserialiser for PostBody, but in a real example there are a lot more fields that would then need to be handled manually.
I've tried creating a custom ClientData deserializer, but I'm struggling to understand the myriad of different types of desrializer interfaces available.
I've got something like this so far:
#Value
#Builder
#JsonDeserialize(builder = PostBody.PostBodyBuilder.class)
class PostBody {
byte[] id;
#JsonDeserialize(using = ClientDataBase64Deserializer.class)
ClientData clientData;
#JsonPOJOBuilder(withPrefix = "")
public static class PostBodyBuilder {}
}
// SNIP
class ClientDataBase64Deserializer extends StdScalarDeserializer<ClientData> {
protected ClientDataBase64Deserializer() {
super(ClientData.class);
}
#Override
public ClientData deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
byte[] value = Base64.getDecoder().decode(jsonParser.getText());
System.out.println(new String(value)); // prints stringified JSON
jsonParser.setCurrentValue(/* somehow convert stringified JSON to a Tree Node? */ value);
return deserializationContext.readValue(jsonParser, ClientData.class);
}
}
I'd be grateful for any ideas on how to progress with this example, or some other mechanism that I may be missing to solve this problem?
Cheers
In true SO fashion, I managed to solve my problem minutes after I asked the question.
This implementation of ClientDataBase64Deserializer and PostBody works as expected:
#Value
#Builder
#JsonDeserialize(builder = PostBody.PostBodyBuilder.class)
public class PostBody {
byte[] id;
ClientData clientData;
public interface IPostBodyBuilder {
#JsonDeserialize(using = ClientDataBase64Deserializer.class)
PostBody.PostBodyBuilder clientData(ClientData clientData);
}
#JsonPOJOBuilder(withPrefix = "")
public static class PostBodyBuilder implements IPostBodyBuilder {}
}
class ClientDataBase64Deserializer extends StdScalarDeserializer<ClientData> {
private final ObjectMapper objectMapper;
protected ClientDataBase64Deserializer(ObjectMapper objectMapper) {
super(ClientData.class);
this.objectMapper = objectMapper;
}
#Override
public ClientData deserialize(
JsonParser jsonParser, DeserializationContext deserializationContext
) {
byte[] value = jsonParser.readValueAs(byte[].class);
return objectMapper.readValue(value, ClientData.class);
}
}
In my pojo class I have configured a CustomDeserializer with annotation
#JsonDeserialize(using = CustomDeserializer.class)
class Myclass {
private String A;
#JsonIgnore
private String B;
#JsonIgnore
private String C;
private String D;
...
private String Z;
/*getters and setters*/
}
In CustomDeserializer, I want to manage only some of the fields and leave the rest for Jackson to manage.
CustomDeserializer.java
public class CustomDeserializer extends StdDeserializer<Myclass > {
private static final long serialVersionUID = 4781685606089836048L;
public CustomDeserializer() {
super(Myclass.class);
}
#Override
public Myclass deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, IllegalResponseException {
ObjectMapper mapper = (ObjectMapper) jp.getCodec();
ObjectNode root = (ObjectNode) mapper.readTree(jp);
Myclass myClass = mapper.readValue(root.toString(), Myclass.class);
//--- HERE MANAGE FIELD B ---
myClass.setB(myNewB);
//--- HERE MANAGE FIELD C ---
myClass.setC(myNewC);
return myClass;
}
}
This way I run into an infinite loop because of the following line:
mapper.readValue(root.toString(), Myclass.class);
Is there a way to set default behavior when using Jackson so that I can exclude my CustomDeserializer?
The problem is that you will need a fully constructed default deserializer; and this requires that one gets built, and then your deserializer gets access to it. DeserializationContext is not something you should either create or change; it will be provided by ObjectMapper.
To meet your requirement you can start by writing a BeanDeserializerModifier and registering it via SimpleModule.
The following example should work:
public class CustomDeserializer extends StdDeserializer<Myclass> implements ResolvableDeserializer
{
private static final long serialVersionUID = 7923585097068641765L;
private final JsonDeserializer<?> defaultDeserializer;
public CustomDeserializer (JsonDeserializer<?> defaultDeserializer)
{
super(Myclass.class);
this.defaultDeserializer = defaultDeserializer;
}
#Override public Myclass deserialize(JsonParser jp, DeserializationContext ctxt)
throws IOException, JsonProcessingException
{
Myclass deserializedMyclass = (Myclass) defaultDeserializer.deserialize(jp, ctxt);
// custom logic
return deserializedMyclass;
}
// You have to implement ResolvableDeserializer when modifying BeanDeserializer
// otherwise deserializing throws JsonMappingException
#Override public void resolve(DeserializationContext ctxt) throws JsonMappingException
{
((ResolvableDeserializer) defaultDeserializer).resolve(ctxt);
}
public static void main(String[] args) throws JsonParseException, JsonMappingException, IOException
{
SimpleModule module = new SimpleModule();
//Writing a new BeanDeserializerModifier
module.setDeserializerModifier(new BeanDeserializerModifier()
{
#Override public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer)
{
if (beanDesc.getBeanClass() == Myclass.class)
return new CustomDeserializer(deserializer);
return deserializer;
}
});
//register the BeanDeserializerModifier via SimpleModule
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(module);
Myclass myclass = mapper.readValue(new File("d:\\test.json"), Myclass.class);
}
}
I want to use custom JSON deserializer for some classes(Role here) but I can't get it working. The custom deserializer just isn't called.
I use Spring Boot 1.2.
Deserializer:
public class ModelDeserializer extends JsonDeserializer<Role> {
#Override
public Role deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
return null; // this is what should be called but it isn't
}
}
Controller:
#RestController
public class RoleController {
#RequestMapping(value = "/role", method = RequestMethod.POST)
public Object createRole(Role role) {
// ... this is called
}
}
#JsonDeserialize on Role
#JsonDeserialize(using = ModelDeserializer.class)
public class Role extends Model {
}
Jackson2ObjectMapperBuilder bean in Java Config
#Bean
public Jackson2ObjectMapperBuilder jacksonBuilder() {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.deserializerByType(Role.class, new ModelDeserializer());
return builder;
}
What am I doing wrong?
EDIT It is probably caused by #RestController because it works with #Controller...
First of all you don't need to override Jackson2ObjectMapperBuilder to add custom deserializer. This approach should be used when you can't add #JsonDeserialize annotation. You should use #JsonDeserialize or override Jackson2ObjectMapperBuilder.
What is missed is the #RequestBody annotation:
#RestController
public class JacksonCustomDesRestEndpoint {
#RequestMapping(value = "/role", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
public Object createRole(#RequestBody Role role) {
return role;
}
}
#JsonDeserialize(using = RoleDeserializer.class)
public class Role {
// ......
}
public class RoleDeserializer extends JsonDeserializer<Role> {
#Override
public Role deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
// .................
return something;
}
}
There is also another pretty interesting solution which can be helpful in case when you want to modify your JSON body before calling default deserializer. And let's imagine that you need to use some additional bean for that (use #Autowire mechanism)
Let's image situation, that you have the following controller:
#RequestMapping(value = "/order/product", method = POST)
public <T extends OrderProductInterface> RestGenericResponse orderProduct(#RequestBody #Valid T data) {
orderService.orderProduct(data);
return generateResponse();
}
Where OrderProductInterface is:
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonSerialize(include = NON_EMPTY)
#JsonTypeInfo(use = JsonTypeInfo.Id.NAME, visible = true, property = "providerType")
#JsonSubTypes({
#JsonSubTypes.Type(value = OrderProductForARequestData.class, name = "A")
})
public interface OrderProductInterface{}
The code above will provide dynamic deserialization base on filed providerType and validation according to concrete implementation. For better grasp, consider that OrderProductForARequestData can be something like that:
public class OrderProductForARequestData implements OrderProductInterface {
#NotBlank(message = "is mandatory field.")
#Getter #Setter
private String providerId;
#NotBlank(message = "is mandatory field.")
#Getter #Setter
private String providerType;
#NotBlank(message = "is mandatory field.")
#Getter #Setter
private String productToOrder;
}
And let's image now that we want to init somehow providerType (enrich input) before default deserialization will be executed. so the object will be deserialized properly according to the rule in OrderProductInterface.
To do that you can just modify your #Configuration class in the following way:
//here can be any annotation which will enable MVC/Boot
#Configuration
public class YourConfiguration{
#Autowired
private ObjectMapper mapper;
#Autowired
private ProviderService providerService;
#Override
public void setup() {
super.setup();
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new BeanDeserializerModifier() {
#Override
public JsonDeserializer<?> modifyDeserializer(DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
if (beanDesc.getBeanClass() == OrderProductInterface.class) {
return new OrderProductInterfaceDeserializer(providerService, beanDesc);
}
return deserializer;
}
});
mapper.registerModule(module);
}
public static class OrderProductInterfaceDeserializer extends AbstractDeserializer {
private static final long serialVersionUID = 7923585097068641765L;
private final ProviderService providerService;
OrderProductInterfaceDeserializer(roviderService providerService, BeanDescription beanDescription) {
super(beanDescription);
this.providerService = providerService;
}
#Override
public Object deserializeWithType(JsonParser p, DeserializationContext context, TypeDeserializer typeDeserializer) throws IOException {
ObjectCodec oc = p.getCodec();
JsonNode node = oc.readTree(p);
//Let's image that we have some identifier for provider type and we want to detect it
JsonNode tmp = node.get("providerId");
Assert.notNull(tmp, "'providerId' is mandatory field");
String providerId = tmp.textValue();
Assert.hasText(providerId, "'providerId' can't be empty");
// Modify node
((ObjectNode) node).put("providerType",providerService.getProvider(providerId));
JsonFactory jsonFactory = new JsonFactory();
JsonParser newParser = jsonFactory.createParser(node.toString());
newParser.nextToken();
return super.deserializeWithType(newParser, context, typeDeserializer);
}
}
}
I'm using the Flickr API. When calling the flickr.test.login method, the default JSON result is:
{
"user": {
"id": "21207597#N07",
"username": {
"_content": "jamalfanaian"
}
},
"stat": "ok"
}
I'd like to parse this response into a Java object:
public class FlickrAccount {
private String id;
private String username;
// ... getter & setter ...
}
The JSON properties should be mapped like this:
"user" -> "id" ==> FlickrAccount.id
"user" -> "username" -> "_content" ==> FlickrAccount.username
Unfortunately, I'm not able to find a nice, elegant way to do this using Annotations. My approach so far is, to read the JSON String into a Map<String, Object> and get the values from there.
Map<String, Object> value = new ObjectMapper().readValue(response.getStream(),
new TypeReference<HashMap<String, Object>>() {
});
#SuppressWarnings( "unchecked" )
Map<String, Object> user = (Map<String, Object>) value.get("user");
String id = (String) user.get("id");
#SuppressWarnings( "unchecked" )
String username = (String) ((Map<String, Object>) user.get("username")).get("_content");
FlickrAccount account = new FlickrAccount();
account.setId(id);
account.setUsername(username);
But I think, this is the most non-elegant way, ever. Is there any simple way, either using Annotations or a custom Deserializer?
This would be very obvious for me, but of course it doesn't work:
public class FlickrAccount {
#JsonProperty( "user.id" ) private String id;
#JsonProperty( "user.username._content" ) private String username;
// ... getter and setter ...
}
You can write custom deserializer for this class. It could look like this:
class FlickrAccountJsonDeserializer extends JsonDeserializer<FlickrAccount> {
#Override
public FlickrAccount deserialize(JsonParser jp, DeserializationContext ctxt) throws IOException, JsonProcessingException {
Root root = jp.readValueAs(Root.class);
FlickrAccount account = new FlickrAccount();
if (root != null && root.user != null) {
account.setId(root.user.id);
if (root.user.username != null) {
account.setUsername(root.user.username.content);
}
}
return account;
}
private static class Root {
public User user;
public String stat;
}
private static class User {
public String id;
public UserName username;
}
private static class UserName {
#JsonProperty("_content")
public String content;
}
}
After that, you have to define a deserializer for your class. You can do this as follows:
#JsonDeserialize(using = FlickrAccountJsonDeserializer.class)
class FlickrAccount {
...
}
Since I don't want to implement a custom class (Username) just to map the username, I went with a little bit more elegant, but still quite ugly approach:
ObjectMapper mapper = new ObjectMapper();
JsonNode node = mapper.readTree(in);
JsonNode user = node.get("user");
FlickrAccount account = new FlickrAccount();
account.setId(user.get("id").asText());
account.setUsername(user.get("username").get("_content").asText());
It's still not as elegant as I hoped, but at least I got rid of all the ugly casting.
Another advantage of this solution is, that my domain class (FlickrAccount) is not polluted with any Jackson annotations.
Based on #MichaĆ Ziober's answer, I decided to use the - in my opinion - most straight forward solution. Using a #JsonDeserialize annotation with a custom deserializer:
#JsonDeserialize( using = FlickrAccountDeserializer.class )
public class FlickrAccount {
...
}
But the deserializer does not use any internal classes, just the JsonNode as above:
class FlickrAccountDeserializer extends JsonDeserializer<FlickrAccount> {
#Override
public FlickrAccount deserialize(JsonParser jp, DeserializationContext ctxt) throws
IOException, JsonProcessingException {
FlickrAccount account = new FlickrAccount();
JsonNode node = jp.readValueAsTree();
JsonNode user = node.get("user");
account.setId(user.get("id").asText());
account.setUsername(user.get("username").get("_content").asText());
return account;
}
}
You can also use SimpleModule.
SimpleModule module = new SimpleModule();
module.setDeserializerModifier(new BeanDeserializerModifier() {
#Override public JsonDeserializer<?> modifyDeserializer(
DeserializationConfig config, BeanDescription beanDesc, JsonDeserializer<?> deserializer) {
if (beanDesc.getBeanClass() == YourClass.class) {
return new YourClassDeserializer(deserializer);
}
return deserializer;
}});
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(module);
objectMapper.readValue(json, classType);
I made it this way:
public class FlickrAccount {
private String id;
#JsonDeserialize(converter = ContentConverter.class)
private String username;
private static class ContentConverter extends StdConverter<Map<String, String>, String> {
#Override
public String convert(Map<String, String> content) {
return content.get("_content"));
}
}
}
You have to make Username a class within FlickrAccount and give it a _content field