ModelMapper: Incorect property mapping from null objects - java

I'm trying to map source object which property is set to null to destination object of which this property is set to another object.
Expected result would be that property of destination object will be null after mapping. Instead of that, this property is set to an object and all of its properties are set to null.
Here is an example:
public class ModelMapperTest {
public static void main(String[] args) {
ModelMapper modelMapper = new ModelMapper();
User user = new User();
user.setStatus(null);
StatusDto statusDto = new StatusDto();
statusDto.setId(1);
statusDto.setName("Active");
UserDto userDto = new UserDto();
userDto.setStatus(statusDto);
// user.status=null, userDto.status=StatusDto(id=1, name="Active")
modelMapper.map(user, userDto);
System.out.println("user = " + user);
System.out.println("userDto = " + userDto);
}
#Getter
#Setter
#ToString
public static class User {
private Status status;
}
#Getter
#Setter
#ToString
public static class Status {
private Integer id;
private String name;
}
#Getter
#Setter
#ToString
public static class UserDto {
private StatusDto status;
}
#Getter
#Setter
#ToString
public static class StatusDto {
private Integer id;
private String name;
}
}
Output:
user = ModelMapperTest.User(status=null)
userDto = ModelMapperTest.UserDto(status=ModelMapperTest.StatusDto(id=null, name=null))
Is it possible to somehow configure model mapper to sets UserDto.status to null?

I know this is an older question and you seem to have moved on to a different library, but I had the same problem recently and came up with this solution (building on your example):
Converter<?, ?> preserveNullConverter = (context) ->
context.getSource() == null
? null
: modelMapper.map(context.getSource(), context.getDestinationType());
modelMapper.createTypeMap(User.class, UserDto.class)
.addMappings(mapper -> mapper.using(preserveNullConverter).map(User::getStatus, UserDto::setStatus));
It's not ideal because the .addMappings part needs to be done for every property where the issue occurs, but at least the Converter can be reused.

Related

Using Java records for (de-)serializing complex/nested objects to JSON

Given the following JSON...
{
"loginData": {
"username": "foobar",
"password": "secret"
},
"personalData": {
"givenName": "Foo",
"familyName": "Bar"
}
}
This is what my POJO (with some maybe verbose Lombok annotations) looks like. It gets (de-)serialized by Jackson:
#Builder
#Getter
#NoArgsConstructor
#AllArgsConstructor
public class User {
private LoginData loginData;
private PersonalData personalData;
#Builder
#Getter
#NoArgsConstructor
#AllArgsConstructor
public static class LoginData {
private String username;
private String password;
}
#Builder
#Getter
#NoArgsConstructor
#AllArgsConstructor
public static class PersonalData {
private String givenName;
private String familyName;
}
}
I wonder if it is possible to replicate this structure using the relatively new concept of a Java record. I'm aware of a simple record like:
public record LoginData(String username, String password) {}
But can records also be used for nested/complex objects which shall be converted to JSON with Jackson?
From what I understood you can use records similar to normal objects/classes. This also means you can use records as parameters for other records. I created a simple example for converting your input JSON string to a nested object with records:
Interface defining three records:
public interface IRecord {
record LoginData(String username, String password) {};
record PersonalData(String givenName, String familyName) {};
record Data(LoginData loginData, PersonalData personalData) {};
}
Main class to test:
public class Main {
public static void main(String[] args) throws JsonProcessingException {
ObjectMapper mapper = new ObjectMapper();
String jsonInString = "{\"loginData\": {\"username\": \"foobar\",\"password\": \"secret\"}, \"personalData\": {\"givenName\": \"Foo\", \"familyName\": \"Bar\"}}";
IRecord.Data user = mapper.readValue(jsonInString, IRecord.Data.class);
System.out.println(user.loginData().username());
}
}
Output:
foobar

How to make serialize missing fields in JSON String with jackson

I am reading a JSON String where every field is optional and i need to either get the values from config or set it to a default value.
Say my JSON is -
{
"action": {
"onWarning":{
"alert":{
"isEnabled": true,
"name": "DVS.sd_service.data-validation.bigdata.warning"
}
},
"onError":{
"alert":{
"isEnabled": false,
"name": "DVS.sd_service.data-validation.bigdata.error"
}
}
}
}
And my code to read this JSON is -
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.registerModule(new Jdk8Module());
JobConfig jobConfig = mapper.readValue(contentJson, JobConfig.class);
And below are my wrapper classes -
#AllArgsConstructor
#JsonIgnoreProperties(ignoreUnknown = true)
public class JobConfig {
public Optional<AlertConfig> action;
}
#AllArgsConstructor
#JsonIgnoreProperties(ignoreUnknown = true)
public class AlertWrapper {
public Optional<Alert> alert;
}
#AllArgsConstructor
#JsonIgnoreProperties(ignoreUnknown = true)
public class AlertConfig {
public Optional<AlertWrapper> onSuccess;
public Optional<AlertWrapper> onWarning;
public Optional<AlertWrapper> onError;
}
#AllArgsConstructor
#JsonIgnoreProperties(ignoreUnknown = true)
public class Alert{
public Optional<Boolean> isEnabled;
public Optional<String> name;
}
Now the objective here is to read OnError.alert.isEnabled however if this field or the entire onError part is not available then we have to set it to default True
The function i have written for this is :-
private Optional<Boolean> getErrorIsEnabled(JobConfig jobConfig ) {
Optional<Boolean> isEnabled = Optional.empty();
if(jobConfig.action.isPresent()) {
if(jobConfig.action.get().onError.isPresent()){
if(jobConfig.action.get().onError.get().alert.isPresent()) {
if(jobConfig.action.get().onError.get().alert.get().isEnabled.isPresent()){
isEnabled= jobConfig.action.get().onError.get().alert.get().isEnabled;
}
}
}
}
return isEnabled;
}
This way i get to find if the vale is available in config by calling the above function -
getErrorIsEnabled(jobConfig).orElse(true)
The problem is (calling the above function) this works but looks way too ugly to keep on checking if the fields are available or not at every level by calling the isPresent() funtion.
The User can entirely skip the OnError Part, or action Part, or just the alert part, or the alert.isEnabled part. There has to be a better way to acieve this! Open to suggestions or improvements or try a different approach alltogether.
What you should do is create default values for the fields like
private Optional<Boolean> isEnabled = Optional.empty();
private Optional<String> name = Optional.empty();
public Alert(Optional<Boolean> isEnabled , Optional<String> name){
this.isEnabled=isEnabled;
this.name=name;
}
And for every Object like this -
private final Alert alertDetail = new Alert(Optional.empty(), Optional.empty());
private final AlertWrapper alertWrapper = new AlertWrapper(alertDetail);
private AlertWrapper onSuccess = alertWrapper;
private AlertWrapper onWarning = alertWrapper;
private AlertWrapper onError = alertWrapper;
You will have to create constructor so you will have to change your lombok to #NoArgsConstructor also use #Getter #Setter so you can call your objects like below -
jobConfig.getAction().getOnWarning().getAlert().getIsEnabled().orElse(true)

Why is Jackson using the wrong element name when serializing?

I have a Object that I would like Jackson to serialize like this...
<AccountsResponse>
<accounts>
<account/>
<account>
<userId>user</userId>
...
</account>
</accounts>
</AccountsResponse>
To try this I create the following class...
#Getter
#Setter
#ToString
#JsonInclude(JsonInclude.Include.NON_EMPTY)
public class Payload {
#JacksonXmlProperty(localName = "errormessage")
private String errorMessage;
}
#Getter
#Setter
#ToString
public class AccountsResponse extends Payload{
#JsonIgnore
private static Logger LOGGER = LogManager.getLogger(AccountsResponse.class);
#JacksonXmlProperty(localName = "accounts")
private List<Account> accounts = Lists.newArrayList();
public static AccountsResponse mapFromResultSet(ResultSet rs)
throws SQLException
{
AccountsResponse response = new AccountsResponse();
do {
Account acct = Account.mapFromResultSet(rs);
response.getAccounts().add(acct);
} while (rs.next());
return response;
}
public String toXml() throws JsonProcessingException {
ObjectMapper mapper = new XmlMapper();
return mapper.writeValueAsString(this);
}
}
#Getter
#Setter
#JsonInclude(JsonInclude.Include.NON_NULL)
public class Account extends ResultSetParser{
...
}
But when I serialize I get...
<AccountsResponse>
<accounts>
<accounts/>
<accounts>
<userId>user</userId>
...
</accounts>
</accounts>
</AccountsResponse>
As you can see the problem here is the child tags should be account but in fact are accounts. I tried hacking around with the localname but can't find the right mixture of VooDoo. What am I doing wrong?
I would change annotations on account list in AccountsResponse:
public class AccountsResponse extends Payload{
#JacksonXmlElementWrapper(localName = "accounts")
#JacksonXmlProperty(localName = "account")
private List<Account> accounts = Lists.newArrayList();
}

Java MongoDB cascading exception

I have a question about Cascading in MongoDB;
So. My project is based on Java 10 and Spring Boot 2.0.5 and Lombok.
I've created CascadeSave event listener and here is it
public class CascadeSaveMongoEventListener extends AbstractMongoEventListener {
#Autowired
private MongoOperations mongoOperations;
#Override
public void onBeforeConvert(BeforeConvertEvent event) {
Object source = event.getSource();
ReflectionUtils.doWithFields(source.getClass(), field -> {
ReflectionUtils.makeAccessible(field);
if (field.isAnnotationPresent(DBRef.class) && field.isAnnotationPresent(CascadeSave.class)) {
Object fieldValue = field.get(source);
if (fieldValue instanceof Collection<?>) {
Collection collection = (Collection<?>) fieldValue;
for (Object o : collection) {
mongoOperations.save(o);
}
} else {
mongoOperations.save(fieldValue);
}
}
});
}
}
Event listener is also included in mongo configuration like this:
#Bean
public CascadeSaveMongoEventListener cascadingMongoEventListener() {
return new CascadeSaveMongoEventListener();
}
And this how domain class looks like:
#Data
#AllArgsConstructor
#NoArgsConstructor
#Document
public class Article {
#Id
private String id;
#DBRef(lazy = true)
#CascadeSave
private List<Comment> comments;
}
#Data
#AllArgsConstructor
#NoArgsConstructor
#Document
public class Comment {
#Id
private String id;
private String text;
}
The problem is that after saving I've take a look at DB via Mongo Compass and Article.comments is collection of null's, but the comments are saved correctly in separate collection. What is the problem? Thanks!

Serialize two different POJO object with the same id with Jackson

I have these two classes:
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id",scope = Rol.class)
public class Rol extends MyEntity implements Serializable {
private Integer id;
private String rolName;
public Rol(Integer id, String rolName) {
this.id = id;
this.rolName = rolName;
}
...
}
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id",scope = User.class)
public class User extends MyEntity implements Serializable {
private Integer id;
private String name;
private List<Rol> rolList;
public User(Integer id, String name, List<Rol> rolList) {
this.id = id;
this.name = name;
this.rolList = rolList;
}
...
}
and I try to serialize and deserialize the user object as following
Rol rol1 = new Rol(1, "MyRol");
Rol rol2 = new Rol(1, "MyRol");
List<Rol> rolList = new ArrayList();
rolList.add(rol1);
rolList.add(rol2);
user = new User(1, "MyUser", rolList);
ObjectMapper mapper = new ObjectMapper();
String jsonString = mapper.writeValueAsString(user);
User userJson = mappe.readValue(jsonString, User.class);
and the JsonMappingException: Already had POJO for id is produced. Why?
When I review the json result of the serialization I see that the result is
{"id": 1,"name": "MyName","rolList": [{"id": 1,"rolName": "MyRol"},{"id": 1,"rolName": "MyRol"}]}
when the result should be
{"id": 1,"name": "MyName","rolList": [{"id": 1,"rolName": "MyRol"},1]}
because rol1 and rol2 are different instances of the same POJO identifier with id 1.
How can I avoid the JsonMappingException? In my project I have some different instances of the same POJO. I can guarantee that if the id's are equal -> objects are equal.
Excuse me for my bad English.
For anyone returning to this question, it looks like there's option to do this with a custom ObjectIdResolver in Jackson. You can specify this on the #JsonIdentityInfo annotation, e.g. :
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "name",
resolver = CustomObjectIdResolver.class)
Then perhaps wrap the normal SimpleObjectIdResolver class to get going and customise bindItem().
In my case I wanted to avoid overlapping objectIds, so cleared down the references when I started a new Something:
public class CustomObjectIdResolver implements ObjectIdResolver {
private ObjectIdResolver objectIdResolver;
public CustomObjectIdResolver() {
clearReferences();
}
#Override
public void bindItem(IdKey id, Object pojo) {
// Time to drop the references?
if (pojo instanceof Something)
clearReferences();
objectIdResolver.bindItem(id, pojo);
}
#Override
public Object resolveId(IdKey id) {
return objectIdResolver.resolveId(id);
}
#Override
public boolean canUseFor(ObjectIdResolver resolverType) {
return resolverType.getClass() == getClass();
}
#Override
public ObjectIdResolver newForDeserialization(Object context) {
return new CustomObjectIdResolver();
}
private void clearReferences() {
objectIdResolver = new SimpleObjectIdResolver();
}
}
Jackson expects in this case different id for different class instances. There has been a previous discussion at github here. Overriding hashCode and equals will not help. Object references must match for equal id.
Options
Reuse Rol instances instead of making new ones with equal fields. As a bonus you will also save memory.
Modify the application logic so that it doesn't depend on #JsonIdentityInfo

Categories