Jackson: Unable to deserialize a map attribute in Request Dto using annotations - java

I want to deserialize this json as Map<string, LocalizationDto>:
{
"DE": {
"name": "name1",
"description": "name1 description"
},
"EN": {
"name": "name2",
"description": "name2 description"
}
}
that represents the parameter "localizations" in this Request Dto:
#Getter
#Setter
#JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
#AllArgsConstructor
public class RequestDto {
#Size(max = 16)
#NotEmpty
private String code;
#NotNull
private Boolean stackable;
#NotNull
private MultipartFile file;
#JsonDeserialize(using = MapDeserializer.class )
#JsonProperty("localizations")
#NotEmpty
private Map<String, LocalizationDto> localizations;
#JsonCreator
public RequestDto() {
}
public static class MapDeserializer extends JsonDeserializer <Map<String, LocalizationDto>> {
public MapDeserializer() {
super();
}
#Override
public Map<String, LocalizationDto> deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
throws IOException {
ObjectMapper mapper = (ObjectMapper) jsonParser.getCodec();
if (jsonParser.getCurrentToken().equals(JsonToken.START_OBJECT)) {
return mapper.readValue(jsonParser, new TypeReference<>() {
});
} else {
//consume this stream
mapper.readTree(jsonParser);
return new HashMap<>();
}
}
}
}
The LocalizationDto class is:
#Getter
#Setter
#JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY)
#NoArgsConstructor
#AllArgsConstructor
public class LocalizationDto {
#NotBlank
#Size(max = 1024)
public String name;
#NotBlank
#Size(max = 1024)
public String description;
}
the Deserialization should be triggered by this Rest Request:
#PostMapping(value = "/create", consumes = MediaType.MULTIPART_FORM_DATA_VALUE , produces = MediaType.APPLICATION_JSON_VALUE)
public responseDto createEntity(Authentication authentication,
#ModelAttribute #Valid RequestDto requestDto) throws IOException {
return service.createEntity(requestDto));
}
}
After running this request I got a problem with the attribut "localizations":
"org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult:
org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult
default message [Failed to convert property value of type 'java.lang.String' to required type 'java.util.Map' for property 'localizations';
nested exception is java.lang.IllegalStateException:
It seems that Jackson is not considering my custom deserializer class MapDeserializer or any annotation is missing.
Thank you for the support

Related

RabbitMQ Add an object to another one coming from queue in Spring

I have this json object that is mapped to this model class:
Json:
{
"type": "NEW",
"operation": "NEW",
"id": 1,
"entity": "DOCUMENT",
"entityType": "NIE",
"documents": {
"id": 1,
"additionals": {
"issuing_authority": "Spain",
"country_doc": "ES",
"place_of_birth": "",
"valid_from": "1995-08-09",
"valid_to": "0001-01-01"
},
"code": "X12345",
"typeDocument": "NIE"
}
}
The model class:
public class PeopleDocumentDTO {
private String processType;
private String operation;
private String entity;
private String entityType;
private Long id;
private Document document;
#Getter
#Setter
class Customer {
private String systemId;
private String customerId;
}
private List<Customer> customers;
}
The thing is that model class also includes a list of customers that have to be added and its coming from another microservice here:
#Service
public class WebClientService {
public Mono<CuCoPerson> getCuCoPerson(Integer cucoId, String GS_AUTH_TOKEN) {
WebClient webClient = WebClient.create();
return webClient.get()
.uri(GET_RELATION_BY_ID + cucoId)
.header("Accept", "application/json")
.header("Authorization", GS_AUTH_TOKEN)
.retrieve()
.bodyToMono(CuCoPerson.class)
.map(cuCoPerson -> {
List<CustomerRelation> matches = cuCoPerson.getRelatedCustomers()
.stream()
.filter(relation -> relation.getSystemId().equals(400) || relation.getSystemId().equals(300) || relation.getSystemId().equals(410))
.filter(relation -> relation.getCustomerId().contains("F"))
.collect(Collectors.toList());
cuCoPerson.setRelatedCustomers(matches);
return cuCoPerson;
});
}
}
Last but not least, this is my listenerClass where I map my message:
#RabbitListener(queues = "${event.queue}")
public void receivedMessage(Message message) throws JsonProcessingException {
String json = "";
json = new String(message.getBody(), StandardCharsets.UTF_8);
System.out.println(json);
logger.info("Received message: {}", json);
ObjectMapper objectMapper = new ObjectMapper();
PeopleDocumentDTO dto = objectMapper.readValue(json, PeopleDocumentDTO.class);
So, how can I add this cuCoPerson to my model DTO?

javax's #Valid annotation usage scenario

Is it possible to use #Valid (javax.validation.Valid) in below scenario?
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.validation.Valid;
import com.incident.tool.model.IncidentModel;
#Service
public class JsonStringToObjectConverter {
public IncidentModel convertToObject(String json) throws JsonMappingException, JsonProcessingException {
#Valid
IncidentModel incidentModel = new ObjectMapper().readValue(json, IncidentModel.class);
return incidentModel ;
}
}
Here JsonStringToObjectConvertor is taking in JSON in form of String and mapping it to IncidentModel class. I have defined few validations in IncidentModel in below manner and I want to validate the fields mapped by ObjectMapper in IncidentModel before proceeding further:
#Component
#Getter
#Setter
#ToString
#NoArgsConstructor
#AllArgsConstructor
public class IncidentModel extends IncidentInfo {
#NotEmpty
private String empId;
#NotEmpty
#Size(min = 2, max = 30)
private String empName;
#NotEmpty
private String title;
private String description;
private String assignedTo;
private String severity;
private String incidentNumber;
private String dateCreated;
private String dateClosed;
private String closingNotes;
}
It does not seem to work in the above format, is there any alternative to use the #Valid in the convertToObject method?
Thanks for your help.
You can do something as follows:
#Service
public class JsonStringToObjectConverter {
public IncidentModel convertToObject(String json) throws JsonMappingException, JsonProcessingException {
IncidentModel incidentModel = new ObjectMapper().readValue(json, IncidentModel.class);
ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
Validator validator = factory.getValidator();
Set<ConstraintViolation<IncidentModel>> errors = validator.validate(incidentModel);
return incidentModel;
}
}
You could then optimize this and make ValidatorFactory factory and Validator validator instance variables of JsonStringToObjectConverter so that you don't recreate them every time you call convertToObject method.

Remove method name from posted comment

I made a code which add comments on my localhost:3000 but its parsing to much info i want to remove "commentModel" but if i remove it from CommentRq class i get errors
comment example:
{ "commentModel": { "comment": "komentarz", "date": "3/6/19 9:34 AM" }, "id": 1}
i want it to be { "comment": "komentarz", "date": "3/6/19 9:34 AM" }, "id": 1 }
CommentRq
#AllArgsConstructor
#NoArgsConstructor
#Data
#Builder
public class CommentRq {
#JsonProperty(access = JsonProperty.Access.READ_ONLY)
private CommentModel commentModel;
#AllArgsConstructor
#NoArgsConstructor
#Data
#Builder
public static class CommentModel {
#JsonProperty("comment")
String resourceName;
#JsonProperty("date")
String resourceNamed;
}
}
CommentBody
public class CommentBody {
Date now = new Date();
#JsonInclude(JsonInclude.Include.NON_NULL)
public CommentRq RequestCommentBody() {
return CommentRq.builder()
.commentModel(new CommentRq.CommentModel(
"komentarz",
(DateFormat.getInstance().format(now))
))
.build();
}
}
Here i create comment
Interface.PostComment postComment = Feign.builder()
.client(new OkHttpClient())
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.logger(new Slf4jLogger(Interface.PostComment.class))
.logLevel(Logger.Level.FULL)
.target(Interface.PostComment.class, "http://localhost:3000/comments/");
#When("i try to add a comment")
public void postComment() {
Map<String, Object> headermap = new HashMap<>();
headermap.put("Content-Type", "application/json");
CommentBody requestComment = new CommentBody();
CommentRes commentRes = postComment.postComment(headermap, requestComment.RequestCommentBody());
id = commentRes.getId();
LOGGER.info("Created: " + DateFormat.getInstance().format(now));
}
You can annotate your private CommentModel commentModel with #JsonUnwrapped. It will unwrap your commentModel object and write its fields to the root of the json. This will handle your specific case. But you can revise your request structure as well: put CommentModel fields into CommentRq and map CommentModel object to CommentRq object.

JsonIgnoreProperties doesn't work with JsonCreator constructor

I have the following entity which I use as a target POJO for one of the requests to a controller:
Entity
#Table(name="user_account_entity")
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonSerialize(using = UserAccountSerializer.class)
public class UserAccountEntity implements UserDetails {
//...
private String username;
private String password;
#PrimaryKeyJoinColumn
#OneToOne(mappedBy= "userAccount", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private UserEntity user;
#PrimaryKeyJoinColumn
#OneToOne(mappedBy= "userAccount", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private UserAccountActivationCodeEntity activationCode;
#JsonCreator
public UserAccountEntity(#JsonProperty(value="username", required=true) final String username, #JsonProperty(value="password", required=true) final String password) {
//....
}
public UserAccountEntity() {}
//.....
}
When I put unexpected fields in the request, it throws MismatchedInputException and fails with this message:
com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot construct instance of `com.myproject.project.core.entity.userAccountActivationCode.UserAccountActivationCodeEntity` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('9WL4J')
at [Source: (PushbackInputStream); line: 4, column: 20] (through reference chain: com.myproject.project.core.entity.userAccount.UserAccountEntity["activationCode"])
In the controller I have:
#InitBinder
public void binder(WebDataBinder binder) {
binder.addValidators(new CompoundValidator(new Validator[] {
new UserAccountValidator(),
new UserAccountActivationCodeDTOValidator() }));
}
And the endpoint that I make request to is:
#Override
public UserAccountEntity login(#Valid #RequestBody UserAccountEntity account,
HttpServletResponse response) throws MyBadCredentialsException, InactiveAccountException {
return userAccountService.authenticateUserAndSetResponsenHeader(
account.getUsername(), account.getPassword(), response);
}
Update 1
The code for UserAccountSerializer:
public class UserAccountSerializer extends StdSerializer<UserAccountEntity> {
public UserAccountSerializer() {
this(null);
}
protected UserAccountSerializer(Class<UserAccountEntity> t) {
super(t);
}
#Override
public void serialize(UserAccountEntity value, JsonGenerator gen,
SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeStringField("id", value.getId());
gen.writeStringField("username", value.getUsername());
gen.writeEndObject();
}
}
The error is triggered because you have in your json :
"activationCode" : "9WL4J"
However Jackson does not know how to map the string "9WL4J" to the object UserAccountActivationCodeEntity
I guess the string "9WL4J" is the value of the primary key id of UserAccountActivationCodeEntity, in which case you should have in the json :
"activationCode" : {"id" : "9WL4J"}
If it is not the case use a custom Deseralizer to tell Jackson how to map the string to the object. You could use #JsonDeserialize on your entity.

Custom Jackson Deserializer to handle a property of any bean

I have a class called Channel which will have roles property as follows
public class Channel{
private int id;
private String roles;
}
And my JSON from client would be
{
"id":"12345787654323468",
"roles":[
{"name":"admin","permissions":["can publish","can reject"]},
{"name":"moderator","permissions":["can publish","can reject"]}
]
}
But when I convert this JSON to Channel object I am getting following exception
com.google.appengine.repackaged.org.codehaus.jackson.map.JsonMappingException: Can not deserialize instance of java.lang.String out of START_ARRAY token
at [Source: java.io.StringReader#6d25f91; line: 1, column: 253] (through reference chain: com.pokuri.entity.Channel["roles"])
Now I want to deserialize this as a string into property roles of Channel class. Also can I write single custom deserializer to handle property of JSON array in any bean.
A custom Deserializer can do the trick here. :
class CustomDeserializer extends JsonDeserializer<String> {
#Override
public String deserialize(JsonParser jsonParser, DeserializationContext context) throws IOException, JsonProcessingException {
JsonNode node = jsonParser.readValueAsTree();
return node.toString();
}
}
now to use this in your bean, you have to include it on roles field :
class Channel {
private long id;
#JsonDeserialize(using = CustomDeserializer.class)
private String roles;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getRoles() {
return roles;
}
public void setRoles(String roles) {
this.roles = roles;
}
}
Note : I have taken value of id as long as it was showing error for int, as value is too large in id attribute.
Now ObjectMapper can easily deserialize your JSON to Channel class :
String json = "{\"id\":\"12345787654323468\",\"roles\":[{\"name\":\"admin\",\"permissions\":[\"can publish\",\"can reject\"]},{\"name\":\"moderator\",\"permissions\":[\"can publish\",\"can reject\"]}]}";
ObjectMapper mapper = new ObjectMapper();
Channel channel = mapper.readValue(json, Channel.class);
System.out.println("Roles :"+channel.getRoles());

Categories

Resources