Deserialize list of objects with inner list with Jackson - java

I have such an entity:
public class User {
private Long id;
private String email;
private String password;
private List<Role> roles;
//set of constructors, getters, setters...
and related JSON:
[
{
"email": "user#email",
"roles": ["REGISTERED_USER"]
}
]
I'm trying to deserialize it in Spring MVC #Controller in such way:
List<User> users = Arrays.asList(objectMapper.readValue(multipartFile.getBytes(), User[].class));
Before adding List<Role> it worked perfect, but after I still have no luck. It seems I need some custom deserializer, could you help with approach for solving? Thank you!

If you have access to Role class you can just add such constructor:
private Role(String role) {
this.role = role;
}
or static factory methods:
#JsonCreator
public static Role fromJson(String value){
Role role = new Role();
role.setRole(value);
return role;
}
#JsonValue
public String toJson() {
return role;
}
Otherwise you will have to write custom deserealizer and register it on object mapper like this:
public static class RoleSerializer extends JsonSerializer {
#Override
public void serialize(Object value, JsonGenerator gen, SerializerProvider serializers) throws IOException, JsonProcessingException {
gen.writeString(((Role) value).getRole());
}
}
public static class RoleDeserializer extends JsonDeserializer {
#Override
public Role deserialize(JsonParser jsonParser,DeserializationContext deserializationContext) throws IOException {
ObjectCodec oc = jsonParser.getCodec();
JsonNode node = oc.readTree(jsonParser);
Role role = new Role();
role.setRole(node.asText());
return role;
}
}
Here is demo: https://gist.github.com/varren/84ce830d07932b6a9c18
FROM: [{"email": "user#email","roles": ["REGISTERED_USER"]}]
TO OBJ: [User{id=null, email='user#email', password='null', roles=Role{role='REGISTERED_USER'}}]
TO JSON:[{"email":"user#email","roles":["REGISTERED_USER"]}]
Ps:
If you use ObjectMapper like this
Arrays.asList(objectMapper.readValue(multipartFile.getBytes(), User[].class));
then code from demo will work, but you will probably have to set custom Bean in Spring for jackson ObjectMapper to make RoleDeserializer and RoleSerializer work everywhere. Here is more info:
Spring, Jackson and Customization (e.g. CustomDeserializer)

Related

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.

Ignoring Jackon JsonProperty Access for Unit Tests

I use Jackson for serialization/deserialization with my Spring Boot project.
I have a DTO object with the following structure,
public class TestDTO implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
#JsonProperty(access = JsonProperty.Access.READ_ONLY)
private UUID certificateId;
#NotNull
private Long orgId;
#NotNull
private CertificateType certificateType;
#JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
#Valid
#NotNull
private PublicCertificateDTO publicCertificate;
#JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
#Valid
private PrivateCertificateDTO privateCertificate;
#JsonProperty(access = JsonProperty.Access.READ_ONLY)
private ZonedDateTime expiryDate;
#JsonProperty(access = JsonProperty.Access.READ_ONLY)
private ZonedDateTime createdDate;
#JsonProperty(access = JsonProperty.Access.READ_ONLY)
private ZonedDateTime updatedDate;
}
Serialization of this object in my unit tests with the following method,
public static byte[] convertObjectToJsonBytes(TestDTO object)
throws IOException {
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
JavaTimeModule module = new JavaTimeModule();
mapper.registerModule(module);
return mapper.writeValueAsBytes(object);
}
causes fields with WRITE_ONLY access to get ignored (for obvious reasons). So in the serialized object I see null values for publicCertificate and privateCertificate.
I did try setting mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY)
Is there any other way to ignore these properties for Unit Tests ?
While the solution specified works, it is an overkill for the requirement. You don't need custom serializers if all you want is to override annotations. Jackson has a mixin feature for such trivial requirements
Consider the following simplified POJO:
public class TestDTO
{
public String regularAccessProperty;
#JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
public String writeAccessProperty;
}
If you want to override the #JsonProperty annotation, you create another POJO that has a variable with the exact same name (or same getter/setter names):
// mixin class that overrides json access annotation
public class UnitTestDTO
{
#JsonProperty(access = JsonProperty.Access.READ_WRITE)
public String writeAccessProperty;
}
You associate the original POJO and the mixin via a Simplemodule:
simpleModule.setMixInAnnotation(TestDTO.class, UnitTestDTO.class);
Is there any other way to ignore these properties for Unit Tests ?
Solution: In your convertObjectToJsonBytes method, you can use:
mapper.disable(MapperFeature.USE_ANNOTATIONS);
Reference: MapperFeature.USE_ANNOTATIONS
/**
* Feature that determines whether annotation introspection
* is used for configuration; if enabled, configured
* {#link AnnotationIntrospector} will be used: if disabled,
* no annotations are considered.
*<p>
* Feature is enabled by default.
*/
USE_ANNOTATIONS(true),
Note: This will disable all annotations for given ObjectMapper.
Another solution is to override the annotation inspector with a simple custom class. That would be the minimal example:
ObjectMapper mapper = new ObjectMapper().setAnnotationIntrospector(new JacksonAnnotationIntrospector() {
#Override
public JsonProperty.Access findPropertyAccess(Annotated m) {
return null;
}
});
Other solution for Spring Boot #Autowired object mappers:
Use a dedicated class so it's reusable and more readable:
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.introspect.JacksonAnnotationIntrospector;
public class IgnoreReadOnlyFieldsAnnotationInspector extends JacksonAnnotationIntrospector {
#Override
public JsonProperty.Access findPropertyAccess(Annotated m) {
return null;
}
}
Within the test use #BeforeEach (or her older friends)
public class AmazingTest {
#Autowired
ObjectMapper mapper;
#BeforeEach
void beforeAll(){
// need to copy because the autowired mapper in test and the object mapper in code under test are the same instance
mapper = objectMapper.copy();
mapper.setAnnotationIntrospector(new IgnoreReadOnlyFieldsAnnotationInspector());
}
}
This was solved by adding a custom serializer for the JUnit tests.
So for TestDTO I added the serializer as below.
private class TestJsonSerializer extends StdSerializer<TestDTO> {
public TestJsonSerializer() {
this(null);
}
public TestJsonSerializer(Class<TestDTO> t) {
super(t);
}
#Override
public void serialize(TestDTO value, JsonGenerator gen, SerializerProvider provider) throws IOException {
gen.writeStartObject();
gen.writeNumberField("orgId", value.getOrgId());
gen.writeStringField("certificateType", value.getCertificateType().getType());
if (value.getPublicCertificate() != null) {
gen.writeObjectField("publicCertificate", value.getPublicCertificate());
}
if (value.getPrivateCertificate() != null) {
gen.writeObjectField("privateCertificate", value.getPrivateCertificate());
}
gen.writeObjectField("expiryDate", value.getExpiryDate());
gen.writeObjectField("createdDate", value.getCreatedDate());
gen.writeObjectField("updatedDate", value.getUpdatedDate());
gen.writeEndObject();
}
}
I then added,
ObjectMapper mapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
simpleModule.addSerializer(TestDTO.class, new TestJsonSerializer());
mapper.registerModule(simpleModule);
Similarly added and registered custom serializers for nested objects, publicCertificate and privateCertificate.
Here a simple example
#ToString
#Getter
#Setter
public class Account implements Cloneable {
#JsonProperty(access = Access.WRITE_ONLY)
private Integer accountId;
private String accountType;
private Long balance;
public AccountTest clone() {
AccountTest test = new AccountTest();
test.setAccountId(this.accountId);
test.setAccountType(this.accountType);
test.setBalance(this.balance);
return test;
}
}
#ToString
#Getter
#Setter
public class AccountTest {
private Integer accountId;
private String accountType;
private Long balance;
}
public static void main(String[] args) {
ObjectMapper mapper = new ObjectMapper();
try {
Account account = new Account();
account.setAccountId(1999900);
account.setAccountType("Saving");
account.setBalance(2433l);
AccountTest accountTest = account.clone();
System.out.println(account);
byte[] accountBytes = mapper.writeValueAsBytes(account);
System.out.println(new String(accountBytes));
byte[] accountTestBytes = mapper.writeValueAsBytes(accountTest);
System.out.println(new String(accountTestBytes));
} catch (IOException e) { }
}
}

Is it possible to use variable as #JsonRootName?

I have a spring project and class like that and want to produce json with root name as type. Here is an example:
public class Person {
private String type; //worker
private String name; //Dennis
private String surname; //Ritchie
}
Result should be:
{"worker" : {
"name" : "Dennis" ,
"surname" : "Ritchie"
}
}
Can I do it with Json tags like #JsonRootName or should I write a Class for worker and extend Person class (There are 3 different types)?
You can implement a custom Serializer when you need to serialize a object into a JSON with a different form:
public class PersonSerializer extends JsonSerializer<Person> {
#Override
public void serialize(Person person, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonProcessingException {
jgen.writeStartObject();
jgen.writeFieldName(person.getType());
jgen.writeStartObject();
jgen.writeFieldName("name", person.getName());
jgen.writeFieldName("surname", person.getSurname());
jgen.writeEndObject();
jgen.writeEndObject();
}
}
After that, you can register the serializer on the class:
#JsonSerialize(using = PersonSerializer.class)
public class Person {
private String type;
private String name;
private String surname;
}

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());

How to properly deserialise ClientResponse entity to List of POJOs

I am attempting to deserialise a response entity into a list of POJOs. When I do this directly, using a GenericType like so:
private List<UserRole> extractMembersDirectly(final ClientResponse response) {
return response.getEntity(new GenericType<List<UserRole>>() {});
}
I get this exception:
com.sun.jersey.api.client.ClientHandlerException: com.fasterxml.jackson.databind.JsonMappingException: Unexpected token (START_OBJECT), expected VALUE_STRING: need JSON String that contains type id (for subtype of java.util.List)
However, I can deserialise successfully when I use an ObjectMapper directly:
private List<UserRole> extractMembersUsingMapper(final ClientResponse response) throws IOException {
String json = response.getEntity(String.class);
ObjectMapper mapper = new ObjectMapperFactory().build();
return mapper.readValue(json, new TypeReference<List<UserRole>>() {});
}
The POJO is just:
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonSnakeCase
public class UserRole {
private UUID id;
public UserRole(#JsonProperty("id") final UUID id) {
this.id = id;
}
public UUID getId() {
return id;
}
Is there a way to directly deserialise from the entity without first deserialising to String?
you can try having a custom serializer on the class itself
Writing the serializer
public class UserRoleSerializer extends JsonSerializer<Item> {
#Override
public void serialize(UserRole userRole, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
jgen.writeStartObject();
jgen.writeStringField("id", userRole.id);
jgen.writeEndObject();
}
}
Now registering the serializer to your class
#JsonSerialize(using = UserRoleSerializer.class)
public class UserRole {
...
}
Then Just an idea, not sure if thats what you looking for

Categories

Resources