JSON Converter
Although Jimmer entities are not POJOs, Jackson serialization can be supported by org.babyish.jimmer.jackson.ImmutableModule
.
If using the Jimmer Spring Boot Starter, this ImmutableModule
will be automatically registered without user intervention.
However, sometimes we need to customize the Jackson serialization behavior of certain properties. We give two examples here.
-
Snowflake ID
-
Data Masking
1. Snowflake ID
Snowflake IDs lead to very large int64 values that unfortunately cannot be represented by JavaScript's number type. Therefore, we have no choice but to serialize such id properties as strings.
Usage
- Java
- Kotlin
@Entity
@public interface Book {
@Id
@GeneratedValue(generatorType = SnowflakeIdGenerator.class)
@JsonConverter(LongToStringConverter.class)
long id();
String name();
...Omit other properties...
}
@Entity
@public interface Book {
@Id
@GeneratedValue(generatorType = SnowflakeIdGenerator.class)
@JsonConverter(LongToStringConverter.class)
long id();
String name();
...Omit other properties...
}
Where SnowflakeIdGenerator
is a user-provided distributed snowflake ID generator implementation, which is not the focus of our discussion.
Here, we discuss the highlighted line:
-
@org.babyfish.jimmer.jackson.JsonConverter
indicates custom Jackson serialization/deserialization behavior for theBook.id
property. -
org.babyfish.jimmer.jackson.LongToStringConverter
means serialize/deserialize the long value as a String.LongToStringConverter
is a built-in Converter in Jimmer. Here we show its source code to quickly understand how to customizeConverter
.package org.babyfish.jimmer.jackson;
public class LongToStringConverter implements Converter<Long, String> {
// How to serialize
@Override
public String output(Long value) {
return Long.toString(value);
}
// How to deserialize
@Override
public Long input(String jsonValue) {
return Long.parseLong(jsonValue);
}
}
Note:
-
Jimmer annotations rather than Jackson annotations are used here, because: This is not only an adjustment of the serialization framework Jackson, but also a useful adjustment of
-
No need to consider null values inside Converter, null values will never be passed as an argument to any Converter method.
Here, bypassing ORM, we demonstrate the effect by simulating data.
- Java
- Kotlin
Book book = Immutables.createBook(draft -> {
draft.setId(1546434604146774987L);
draft.setName("SQL in Action");
});
System.out.println(book);
val book = Book {
id = 1546434604146774987L
name = "SQL in Action"
}
println(book)
Since the toString
of Jimmer entities is Jackson serialization, we can print directly and observe the results of Jackson serialization:
{
"id": "1546434604146774987",
"name": "SQL in Action"
}
We can see that the serialized id is a string rather than a number, thus avoiding the problem that JavaScript clients cannot parse large numbers.
Automatically Applied to IdView Properties
@IdView properties is advanced mapping content that will be explained later. Readers can skip this section for now and come back later.
- Java
- Kotlin
@Entity
public interface Author {
@ManyToMany(mappedBy = "authors")
List<Book> books();
@IdView("authors")
List<Long> bookIds();
...Omit other properties...
}
@Entity
interface Author {
@ManyToMany(mappedBy = "authors")
val books: List<Book>
@IdView("authors")
val bookIds: List<Long>
...Omit other properties...
}
Here, the bookIds
property is not an ordinary List <Long>
collection, but the collection of ids of all associated Book
objects.
Because the Book.id
property has already been annotated with @JsonConverter
, there is no need to specify any annotations for the bookIds
property here to give it this capability.
Similarly, bypassing ORM, demonstrate the effect through simulated data.
- Java
- Kotlin
Author author = Immutables.createAuthor(draft -> {
draft.setBookIds(
Arrays.asList(
1546434604146774987L,
1546434604146774988L
)
);
});
System.out.println(book);
val book = Book {
bookIds = listOf(
1546434604146774987L,
1546434604146774988L
)
}
println(book)
Since the toString of Jimmer entities is Jackson serialization, we can print directly and observe the results of Jackson serialization:
{
"bookIds": [
"1546434604146774987",
"1546434604146774988"
]
}
We can see that all elements of the serialized bookIds
property are strings rather than numbers, thus avoiding the problem that JavaScript clients cannot parse large numbers.
Automatically Applied to DTO
DTO is advanced mapping content that will be explained later. Readers can skip this section for now and come back later.
In the src/main/dto
directory, create any file with the extension .dto
and edit its code as follows
export com.yourcompany.yourproject.model.Book
-> package com.yourcompany.yourproject.model.dto
BookView {
id
name
}
After compiling the project, the generated Java/Kotlin type is:
- Java
- Kotlin
@GeneratedBy(file = "<yourproject>/src/main/dto/Book.dto")
public class BookView {
private String id;
private String name;
...Omit other properties...
}
@GeneratedBy(file = "<yourproject>/src/main/dto/Book.dto")
class BookView(
val id: String,
val name: String
) {
...Omit other properties...
}
We can see that the type of the id property in the automatically generated DTO type is String instead of long.
Finally, we verify the conversion from entity to DTO object as follows
- Java
- Kotlin
Book book = Immutables.createBook(draft -> {
draft.setId(1546434604146774987L);
draft.setName("SQL in Action");
});
System.out.println(new BookView(book));
val book = Book {
id = 1546434604146774987L
name = "SQL in Action"
}
println(BookView(book))
The output is as follows
BookView(
id = "1546434604146774987",
name = "SQL in Action"
)
2. Data Masking
Usage
Assume the User
entity has a phone
property representing the user's phone number. We now perform data masking on this property.
For example, displaying the phone number as 135****6792
.
Data masking is not simply a page rendering feature. It must ensure that real data is not returned in the HTTP requests themselves.
There is no built-in Converter available for this requirement. We need to customize the Converter as follows:
- Java
- Kotlin
public class UserPhoneConverter implements Converter<String, String> {
@Override
public String output(String value) {
return value.substring(0, 3) + "****" + value.substring(7);
}
}
class UserPhoneConverter : Converter<String, String> {
override fun output(value: String): String =
"${value.substring(0,3)}****${value.substring(7)}"
}
Then modify the User
entity simply
- Java
- Kotlin
@Entity
public interface User {
@JsonConverter(UserPhoneConverter.class)
String phone();
}
@Entity
interface User {
@JsonConverter(UserPhoneConverter.class)
val phone: String
}
Support Input
In the above example, the UserPhoneConverter
only overrides the output
method without overriding the input
method.
Because the default implementation of the input
method is to throw an UnsupportedOperationException
exception, deserialization is not supported.
However, in actual projects, Input DTO
can be generated based on entity objects to complete data entry. In this case, the client should enter the exact phone number. Therefore, we can override the input
method as follows.
- Java
- Kotlin
@Override
public String input(String value) {
return value;
}
override fun input(value: String): String =
value