#JsonPropertyOrder names independent of property naming strategy - java

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.

Related

way to change the jsonProperty name in only one use case

I have one model class which has approximately 100 property fields. I need to change the jsonProperty name's for one usage scenario but in all other cases where i'm using this class the property name should not be change. What would be the ideal way to handle this situation.
List<DebitTable> debitTableList = getListData();
Below is the DebitTable class.
#Validated
#Data
#NoArgsConstructor
#AllArgsConstructor
#Getter
#Setter
#Builder
public class DebitTable {
#JsonProperty("debitId")
private Long debitId;
#JsonProperty("debitName")
#Pattern(regexp = "^[a-zA-Z0-9 ]*$")
#Size(max = 10)
private String debitName;
.....
}
In one of the usage which returns this List<DebitTable> , i want to change the jsonProperty name from debitId to "Debit Id" and "debitName" to "Debit Name"...Do i need to create another model class by changing the jsonProperty name,but that would be very difficult to set each and every field from one model DebitTable class to other new model class.

Jackson change property names without copying POJO

Today I have found a problem which I need some advice from you.
I have a model from Frontend application which sends me some Json structure. It uses camelCase.
Then I need to send the same model to another service to trigger some functionality but there is a different Json name convention. It basically uses underscore notation instead of camel case but also there some other random differences so I would not say it is strict underscore.
And here's a problem. When I serialize Json from Frontend, is there a better way to remap those field names instead of creating duplicated model but with different JsonProperty annotation? I would like to avoid this solution because model contains around 15 classes so I would say it's big.
Another solution I was thinking about is JSONATA expression. What do you think?
#Data
#NoArgsConstructor
#AllArgsConstructor
public class ADLayout {
private String firstName;
private LayoutHeader layoutHeader;
private Long projectId;
private List<Worksheet> worksheets;
}
#Data
#NoArgsConstructor
#AllArgsConstructor
public class ADLayoutAnotherService {
#JsonProperty("first_name")
private String firstName;
#JsonProperty("layout_header")
private LayoutHeader layoutHeader;
#JsonProperty("project_id")
private Long projectId;
private List<Worksheet> worksheets;
}
FRONT Model
{
"firstName": "test",
"projectId": 5,
"worksheets": [
{
"sheetUid": "201f1630-c97d-4fec-89b4-7b45b44bcebc",
"sheetHeader": {
"requestedData": "",
"instructions": ""
}
}
]
}
Another service Model
{
"first_name": "test",
"project_id": 5,
"worksheets": [
{
"sheet_uid": "201f1630-c97d-4fec-89b4-7b45b44bcebc",
"sheet_header": {
"requested_data": "",
"INSTRUCTIONS": ""
}
}
]
}
Important thing is that the model sent from Frontend is also stored in database and returned to frontend so #JsonAlias will not work here

Java :: JacksonXmlProperty :: Multiple fields with same name

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!

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

Jackson: referencing an object as a property

In my java spring application, I am working with hibernate and jpa, and i use jackson to populate data in DB.
Here is the User class:
#Data
#Entity
public class User{
#Id
#GeneratedValue
Long id;
String username;
String password;
boolean activated;
public User(){}
}
and the second class is:
#Entity
#Data
public class Roles {
#Id
#GeneratedValue
Long id;
#OneToOne
User user;
String role;
public Roles(){}
}
In the class Roles i have a property of User
and then i made a json file to store the data:
[ {"_class" : "com.example.domains.User", "id": 1, "username": "Admin", "password": "123Admin123","activated":true}
,
{"_class" : "com.example.domains.Roles", "id": 1,"user":1, "role": "Admin"}]
Unfortunately, when i run the app it complains with:
.RuntimeException: com.fasterxml.jackson.databind.JsonMappingException: Can not construct instance of com.example.domains.User: no int/Int-argument constructor/factory method to deserialize from Number value (1)
at [Source: N/A; line: -1, column: -1] (through reference chain: com.example.domains.Roles["user"])
The problem comes from
{"_class" : "com.example.domains.Roles", "id": 1,"user":1, "role": "Admin"}
and when i remove the above line the app works well.
I think, it complains because it cannot make an instance of user.
So, how can i fix it?
Do yourself a favor and stop using your Entities as DTOs!
JPA entities have bidirectional relations, JSON objects don't, I also believe that the responsibilities of an Entity is very different from a DTO, and although joining these responsibilities into a single Java class is possible, in my experience it is a very bad idea.
Here are a couple of reasons
You almost always need more flexibility in the DTO layer, because it is often related to a UI.
You should avoid exposing primary keys from your database to the outside, including your own UI. We always generate an additional uniqueId (UUID) for every publicly exposed Entity, the primary key stays in the DB and is only used for joins.
You often need multiple views of the same Entity. Or a single view of multiple entities.
If you need to add a new entity to a relation with an existing, you will need find the existing one in the database, so posting the new and old object as a single JSON structure has no advantage. You just need the uniqueId of the existing, and then new.
A lot of the problems developers have with JPA, specifically with regards to merging comes from the fact that they receive a detached entity after their json has been deserialized. But this entity typically doesn't have the OneToMany relations (and if it does, it's the parent which has a relation to the child in JSON, but in JPA it is the child's reference to the parent which constitutes the relationship). In most cases you will always need to load the existing version of the entity from the database, and then copy the changes from your DTO into the entity.
I have worked extensively with JPA since 2009, and I know most corner cases of detachment and merging, and have no problem using an Entity as a DTO, but I have seen the confusion and types of errors that occur when you hand such code over to some one who is not intimately familiar with JPA. The few lines you need for a DTO (especially since you already use Lombok), are so simple and allows you much more flexibility, than trying to save a few files and breaking the separation of concerns.
Jackson provide ObjectIdResolver interface for resolving the objects from ids during de-serialization.
In your case you want to resolve the id based from the JPA/hibernate. So you need to implement a custom resolver to resolve id by calling the JPA/hierbate entity manager.
At high level below are the steps:
Implement a custom ObjectIdResolver say JPAEntityResolver (you may extends from SimpleObjectIdResolver). During resolving object it will call JPA entity manager class to find entity by given id and scope(see. ObjectIdResolver#resolveId java docs)
//Example only;
#Component
#Scope("prototype") // must not be a singleton component as it has state
public class JPAEntityResolver extends SimpleObjectIdResolver {
//This would be JPA based object repository or you can EntityManager instance directly.
private PersistentObjectRepository objectRepository;
#Autowired
public JPAEntityResolver (PersistentObjectRepository objectRepository) {
this.objectRepository = objectRepository;
}
#Override
public void bindItem(IdKey id, Object pojo) {
super.bindItem(id, pojo);
}
#Override
public Object resolveId(IdKey id) {
Object resolved = super.resolveId(id);
if (resolved == null) {
resolved = _tryToLoadFromSource(id);
bindItem(id, resolved);
}
return resolved;
}
private Object _tryToLoadFromSource(IdKey idKey) {
requireNonNull(idKey.scope, "global scope does not supported");
String id = (String) idKey.key;
Class<?> poType = idKey.scope;
return objectRepository.getById(id, poType);
}
#Override
public ObjectIdResolver newForDeserialization(Object context) {
return new JPAEntityResolver(objectRepository);
}
#Override
public boolean canUseFor(ObjectIdResolver resolverType) {
return resolverType.getClass() == JPAEntityResolver.class;
}
}
Tell Jackson to use a custom id resolver for a class, by using annotation JsonIdentityInfo(resolver = JPAEntityResolver.class). For e.g.
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id",
scope = User.class,
resolver = JPAObjectIdResolver.class)
public class User { ... }
JPAObjectIdResolver is a custom implementation and will have dependency on other resources( JPA Entity Manager) which might not be known to Jackson. So Jackson need help to instantiate resolver object. For this purpose, you need to supply a custom HandlerInstantiator to ObjectMapper instance. (In my case I was using spring so I asked spring to create instance of JPAObjectIdResolver by using autowiring)
Now de-serialization should work as expected.
Hope this helps.
I have changed the json file to :
[
{"_class" : "com.example.domains.User",
"id": 1,
"username": "Admin",
"password": "123Admin123",
"activated":true
},
{
"_class" : "com.example.domains.Roles",
"id": 1,
"user":{"_class" : "com.example.domains.User",
"id": 1,
"username": "Admin",
"password": "123Admin123",
"activated":true
},
"role": "Admin"
}
]
But i still think, the best ways is using a foreign key to user record.
Any solution is welcomed
If your bean doesn't strictly adhere to the JavaBeans format, Jackson has difficulties.
It's best to create an explicit #JsonCreator constructor for your JSON model bean, e.g.
class User {
...
#JsonCreator
public User(#JsonProperty("name") String name,
#JsonProperty("age") int age) {
this.name = name;
this.age = age;
}
..
}
1-1 mapping of fields works well , but when it comes to complex object mapping , better to use some API.
You can use Dozer Mapping or Mapstruct to map Object instances.
Dozer has spring integration also.
You could specify non default constructors and then use a custom deserialiser.
Something like this should work (it has not been tested).
public class RolesDeserializer extends StdDeserializer<Roles> {
public RolesDeserializer() {
this(null);
}
public RolesDeserializer(Class<?> c) {
super(c);
}
#Override
public Roles deserialize(JsonParser jp, DeserializationContext dsctxt)
throws IOException, JsonProcessingException {
JsonNode node = jp.getCodec().readTree(jp);
long id = ((LongNode) node.get("id")).longValue();
String roleName = node.get("role").asText();
long userId = ((LongNode) node.get("user")).longValue();
//Based on the userId you need to search the user and build the user object properly
User user = new User(userId, ....);
return new Roles(id, roleName, user);
}
}
Then you need to register your new deserialiser (1) or use the #JsonDeserialize annotation (2)
(1)
ObjectMapper mapper = new ObjectMapper();
SimpleModule module = new SimpleModule();
module.addDeserializer(Item.class, new RolesDeserializer());
mapper.registerModule(module);
Roles deserializedRol = mapper.readValue(yourjson, Roles.class);
(2)
#JsonDeserialize(using = RolesDeserializer.class)
#Entity
#Data
public class Roles {
...
}
Roles deserializedRol = new ObjectMapper().readValue(yourjson, Roles.class);
public class Roles {
#Id
#GeneratedValue
Long id;
#JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "id")
#JsonIdentityReference(alwaysAsId = true)
#OneToOne
User user;
String role;
public Roles(){}
}

Categories

Resources