Java :: JacksonXmlProperty :: Multiple fields with same name - java

I'm extending code from an existing Java class that serializes to and from XML. The existing class is somewhat like this:
#Getter
#JacksonXmlRootElement("element")
public class Element {
#JacksonXmlProperty(localName = "type", isAttribute = true)
private String type;
}
The type field has a finite set of possible values so I created an enum Type with all possible values and (to avoid breaking existing functionality) added a new field to the class, like so:
#Getter
#JacksonXmlRootElement("element")
public class Element {
#JacksonXmlProperty(localName = "type", isAttribute = true)
private String type;
#JacksonXmlProperty(localName = "type", isAttribute = true)
#JsonDeserialize(using = TypeDeserializer.class)
private Type typeEnum;
}
This gives me the following error:
Multiple fields representing property "type": Element#type vs Element#typeEnum
I understand why this is a problem cuz when Jackson would try to serialize the class, two fields in my class map onto the same field in the output XML.
I tried adding a #JsonIgnore on one of the fields and it gets rid of the error but has the side effect of not populating the ignored field either. Is there a way to annotate that a field should be deserialized (while reading XML) but not serialized (while writing XML)?
I really need to keep both fields in the class to not disturb any legacy code that might be using the first field, but at the same time allow newer code to leverage the second field.
Thank you!

Related

Spring Data Elasticsearch: Convert a String to an Object (and vice versa) using ValueConverter and dot-notation

I have kind of a combination-follow up question to [1] and [2].
I have a POJO with a field I want to persist in - and read from - Elasticsearch:
#Document
public class MyPojo {
private String level3;
// getters/setters...
}
For convenience and because the property is also being persisted (flatly) into postgres, the property level3 should be a String, however it should be written into ES as a nested object (because the ES index is defined elsewhere).
The current solution is unsatisfactory:
#Document
#Entity
public class MyPojo {
#Column(name = "level3)
#Field(name = "level3", type = FieldType.Keyword)
#ValueConverter(MyConverter.class)
private String level3;
// getters/setters...
}
with the object path "level1.level2.level3" hardcoded within MyConverter, which converts from Map<String, Object> to String (read) and from String to Map<String, Object> (write). Because we potentially need to do this on multiple fields, this is not a really viable solution.
I'd rather do something like this:
#Document
#Entity
public class MyPojo {
#Column(name = "level3)
#Field(name = "level1.level2.level3", type = FieldType.Keyword)
#ValueConverter(MyConverter2.class)
private String level3;
// getters/setters...
}
which does not work (writing works fine, while reading we get the "null is not a map" error from [2]).
Is this at all possible (if I understood [2] correctly, no)? If not, is there another way to achieve what I want without hardcoding and an extra converter per field?
Can I somehow access the #Field annotation within MyConverter (e.g. the name), or can I somehow supply additional arguments to MyConverter?
[1] Spring data elasticsearch embedded field mapping
[2] Spring Elasticsearch: Adding fields to working POJO class causes IllegalArgumentException: null is not a Map

How to customize lombok's superbuilder?

I have an existing data model which was (unfortunately) written with bidirectional relationships. Currently, I'm trying to refactor it using Lombok. I've added the #SuperBuilder annotation, but the generated builder does not call my custom setter methods (the ones that ensure that the bidirectionality remain intact).
After running delombok and investigating the resulting code, it appears that a constructor is created on the class being built that takes an instance of the builder to use to set the values. Unfortunately, it simply assigns the field values directly. So I thought maybe I could just implement that constructor myself, and make the calls to the setters as required. Of course, this does not work. When I build I get an error because there are now apparently two implementations of that same method in my class (in other words SuperBuilder implemented it even though it was already implemented in the class).
Does anyone know how to override that constructor (or any other mechanism that would allow me to get the setters called when constructing my object using the SuperBuilder annotation)?
Edit: added code as requested
The entity class I'm trying to refactor to using lombok is:
#Entity
#Table(name = "APPLICATION_USER", uniqueConstraints = #UniqueConstraint(columnNames = { "PRINCIPAL_NAME", "APPLICATION", "SITE_ID" }))
#AttributeOverrides(#AttributeOverride(name = "id", column = #Column(name = "APP_USER_ID")))
#Filters({ #Filter(name = FilterQueryConstants.SITE_ID_FILTER_NAME, condition = FilterQueryConstants.SITE_ID_FILTER) })
#SuperBuilder
public class ApplicationUser extends User
{
private static final long serialVersionUID = -4160907033349418248L;
#Column(name = "APPLICATION", nullable=false)
private String application;
#ManyToMany(mappedBy = "applicationUsers", targetEntity = Group.class)
#Filters({ #Filter(name = FilterQueryConstants.GROUP_FILTER_NAME, condition = FilterQueryConstants.GROUP_FILTER),
#Filter(name = FilterQueryConstants.SITE_ID_FILTER_NAME, condition = FilterQueryConstants.SITE_ID_FILTER) })
#MappingTransform(operation = DTOSecurityOperation.ASSIGN_GROUP)
#Builder.Default
private Set groups = new HashSet ( );
// Other methods omitted for brevity
When I run the delombok, the resulting constructor looks like the following:
protected ApplicationUser(final ApplicationUserBuilder b) {
super(b);
this.application = b.application;
if (b.groups$set) this.groups = b.groups;
else this.groups = ApplicationUser.$default$groups();
}
So I thought I could just basically copy this code into my ApplicationUser class and modify it to call my setter method when it sets the value for groups (rather than just doing a direct assignment). I was thinking of something like this:
protected ApplicationUser(final ApplicationUserBuilder b) {
super(b);
this.application = b.application;
if (b.groups$set) this.setGroups(b.groups);
else this.setGroups(ApplicationUser.$default$groups());
}
Originally, when using 1.18.8, I was getting an error stating that this constructor already exists. Since updating to 1.18.22, I now get this:
error: cannot find symbol
if (b.groups$set) this.setGroups(b.groups);
^
symbol: variable groups
location: variable b of type ApplicationUserBuilder
Customizing #SuperBuilder only works in more recent lombok version; you should always use the most recent one, which is v1.18.22 at the time of the writing of this answer.
With that version, customizing a #SuperBuilder constructor is possible. However, you are using code as a basis for your constructor that has been delomboked with v1.18.8. That does not work any more with current lombok versions. lombok v1.18.10 introduced that the actual field value for #Default fields are stored in the builder in fields like fieldName$value, not simply fieldName.
Thus, your customized constructor has to look as follows:
protected ApplicationUser(final ApplicationUserBuilder<?, ?> b) {
super(b);
this.application = b.application;
if (b.groups$set) this.setGroups(b.groups$value);
else this.setGroups(ApplicationUser.$default$groups());
}

#JsonPropertyOrder names independent of property naming strategy

We have a model like:
#JsonPropertyOrder({
"id",
"alpha2_code",
"alpha3_code",
"name"
})
#Getter
#Setter
public class Country {
private Long id;
private String alpha2Code;
private String alpha3Code;
private String name;
}
and a ObjectMapper instance like:
var jsonMapper = new ObjectMapper();
jsonMapper.enable(SerializationFeature.INDENT_OUTPUT);
jsonMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
jsonMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
jsonMapper.registerModule(new JavaTimeModule());
to create the following JSON:
{
"id": 1,
"alpha2_code": "NL",
"alpha3_code": "NLD",
"name": "Netherlands",
}
This works all as expected.
What important to mention is that we are using #JsonPropertyOrder to sort the output.
This annotation requires the field names as how they are in the output; thus SNAKE CASE like "alpha2_code" and not "alpha2Code" as the Java property name.
Now we have a requirement to create YAML as out put as well (based on the same model).
But the naming convention for the YAML output needs to be KEBAB CASE.
Is there a smart way to solve this?
What I'm thinking of is to move the #JsonPropertyOrder to mix-ins and to introduce CountrySnakeMixin and CountryKebabMixin mix-in classes and use these in separate object mappers.
For this simple example it seems quite straightforward but with a model of 50 - 100 classes this becomes a maintenance nightmare.
I tested
This annotation requires the field names as how they are in the
output
as not correct:
#JsonPropertyOrder({
"id",
"alpha2Code",
"alpha3Code",
"name"
})
works as intended, also changing the order, both in snake and kebab case.

Springboot: Can DTO be changed at runtime with null values not being present in the object returned from api?

I have a springboot application which is hitting raw api's of the datasource. Now suppose I have a Customer entity with approx 50 fields and I have a raw api for it in which I pass names of the columns and I get the values for that column. Now I am implementing api in springboot which consumes raw api.
I need to implement different api's in springboot for different combinations of the fields of Customer entity and return only those fields setted in object for which user had queried and remove the null valued fields from the object. One way is to implement different dto's for different combinations of the columns of Customer entity. Is there any other way to implement the same in which I don't need to define different dto's for different combinations of the columns of Customer entity in Spring boot ???
You can configure the ObjectMapper directly, or make use of the #JsonInclude annotation:
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
OR
#JsonInclude(JsonInclude.Include.NON_NULL)
#JsonInclude(JsonInclude.Include.NON_NULL)
public class Customer {
private Long id;
private String name;
private String email;
private String password;
public Customer() {
}
// getter/setter ..
}
You can see how to do it with this sample code:
Customer customer = new Customer();
customer.setId(1L);
customer.setName("Vikas");
customer.setEmail("info#vikas.com");
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
String valueAsString = objectMapper.writeValueAsString(customer);
Since the password is left null, you will have an object that does not exist password.
{
"id": 1,
"name": "Vikas",
"email": "info#vikas.com"
}
with Jackson 2.0 serialization you can specify data inclusion on non nulls at different levels, i.e. on the object mapper (with constructor options), the DTO class or DTO class fields (with annotations).
See Jackson annotations here
This can be done using #JsonInclude inside the DTO class. Please refer following code block for ignoring null values.
#JsonInclude(Include.NON_NULL) // ignoring null values
#Data //lombock
#Builder //builder pattern
public class Customer {
private Long id;
private String name;
private String email;
private String password;
}

Convert snake case to camel case for Spring Paging and Sorting

The hibernate entities have the fields named in camel case, but when a DTO constructed from that entity is returned from a REST API we convert the field name to snake case.
There is a generic way to convert every DTO field into snake case with a jackson configuration like so
spring.jackson.property-naming-strategy: SNAKE_CASE
The issue with this is now for example, with Spring paging and sorting if we want to sort by parameter we need to pass the parameter as camel case and not as snake case.
Example:
Entity looks like this:
#Data
#Entity
#Table
public class Entity {
#Id
#Column(name = "id", updatable = false, nullable = false)
#GeneratedValue(generator = "uuid")
#Access(AccessType.PROPERTY)
private UUID id;
#Column(name = "some_text")
private String someText;
}
DTO looks like this:
#Data
public class EntityDTO implements Serializable {
private UUID id;
private String someText;
}
The output JSON is the following:
{
"id": "80fb034a-36c1-4534-a39f-b344fa815a2d",
"some_text": "random text"
}
Now if we would like to call the endpoint with a sort parameter for example:
/entities?sort=some_text&some_text.dir=desc
it will not work because the field in the entity is actually someText and not some_text which is quite confusing because the output is in snake case and not camel case.
So the general question is how to deal with this? Is there a smart way to do it? Some jackson configuration or argument handler configuration? Or do I need to manually convert every single snake case parameter to camel case?
Thank you guys in advance.
you the below property in application.properties file to convert Snake case to camel case.
spring.jackson.property-naming-strategy=SNAKE_CASE
Or you can annotate the attributes as below in case you map only a single class.
#JsonProperty("some_text")
private String someText;
OR annotate the entity as below
#JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy.class)
Below I provide the solution which would solve your problem without the need for definition of mapping for each DTO field name to its corresponding DAO/entity field name. It's using constants and method of com.google.common.base.CaseFormat class from Google Guava library for transformation of strings in one case to another. I assume that you are extracting paging and ordering info from request in instance of org.springframework.data.domain.Pageable class.
public Page<Entity> getEntities(Pageable dtoPageable) {
PageRequest daoPageable = PageRequest.of(
dtoPageable.getPageNumber(),
dtoPageable.getPageSize(),
convertDtoSortToDaoSort(dtoPageable.getSort())
);
return entityRepository.findAll(daoPageable);
}
private Sort convertDtoSortToDaoSort(Sort dtoSort) {
return Sort.by(dtoSort.get()
.map(sortOrder -> sortOrder.withProperty(CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.LOWER_CAMEL, sortOrder.getProperty())))
.collect(Collectors.toList())
);
}

Categories

Resources