Skip to main content

Handle Null Values

Input DTO is used for data input, so it provides powerful control over nullable properties in the objects submitted by the client, and standardizes this capability.

Review: Directly Saving Entity Objects

One of the most important features of Jimmer entities is the strict distinction between unknown data (not specifying object properties) and no data (setting object properties to null).

Let's temporarily set aside the concept of Input DTO and review the differences when directly saving data using Jimmer entities.

  • Setting nullable property to null

    Book book = BookDraft.$.produce(draft -> {
    draft.setId(12L);
    draft.setName("TURING");
    draft.setStoreId(null);
    });
    sqlClient.update(book);

    The following SQL is generated:

    update BOOK
    set
    NAME = ?, /* TURING */
    // highlight-next-line
    STORE_ID = ? /* <null: long> */
    where
    ID = ? /* 12 */`

    As you can see, by explicitly setting the object's property to null and executing the save command with an update operation, the value in the database will be updated to null.

  • Not specifying nullable property at all

    Book book = BookDraft.$.produce(draft -> {
    draft.setId(12L);
    draft.setName("TURING");
    // `storeId` is not specified
    });
    sqlClient.update(book);

    The following SQL is generated:

    update BOOK
    set
    NAME = ? /* TURING */
    // highlight-next-line
    /* `STORE_ID` is not updated */
    where
    ID = ? /* 12 */`

    As you can see, by not setting the object's property and executing the save command with an update operation, the value in the database will not be updated.

info

This distinction is very important.

In the subsequent content of this article, we will no longer discuss what SQL statements the ORM generated because we only need to focus on what the entity object obtained from the conversion of the Input DTO is.

Issues Faced by Input DTO

Now, let's define an Input DTO:

input BookUpdateInput {
id!
name
id(store)
}

For more details on the DTO language, please refer to the relevant chapter. Here, we focus on the Java/Kotlin code automatically generated by the Jimmer pre-compiler based on this DTO code.

The generated code is as follows:

BookUpdateInput.java
@GeneratedBy(file = "<your_project>/src/main/dto/Book.dto")
public class BookUpdateInput implements Input<Book> {

private long id;

private String name;

@Nullable
private Long storeId;

@Override
public Book toEntity() {
...omitted...
}

...other members omitted...
}

In the original entity, the associated property Book.store is nullable. The DTO code does not change this here, so in the generated classes, the field storeId is also nullable.

If the storeId property of the BookUpdateInput object uploaded by the user is null, is the user's intention to update the corresponding foreign key STORE_ID in the database to null, or not to update it at all?

In fact, both of these intentions are common. For a long time, developers have been very casual about the conventions for these two behaviors, and even if the API documentation mentions such details, the format is also very casual. This has led to difficulties in communication and understanding and has caused lasting damage to the industry.

Input DTO provides a standardized definition for this issue, aiming to handle different intentions in a standardized manner.

4 Ways to Handle Nullable Properties

To solve the problem raised above, the DTO language specifies that if a DTO property satisfies both of the following conditions:

  • It is defined in an input type

  • It allows null values

Then, a modifier representing the null handling mode can be added to the DTO property: it can be fixed, static, dynamic, or fuzzy.

For convenience in the subsequent discussion, let's assume that the following Web Controller exists:

@RestController
public class BookController {

@PutMapping("/book")
public void update(
@RequestBody BookUpdateInput input
) {
Book book = input.toEntity();
System.out.println(book);
...subsequent code omitted...
}

...other members omitted...
}

Here, we convert the Input DTO object uploaded by the user into a Jimmer entity and print it out.

We only need to focus on the print result, as mentioned earlier, we only need to focus on what the entity object obtained from the conversion of the Input DTO is, and we do not need to discuss what SQL statements the ORM will generate.

Therefore, the subsequent code is not important and is omitted.

1. fixed

DTO code example:

input BookUpdateInput {
id!
name
fixed id(store)
}

This mode can also be called the super static mode.

  • It does not allow the user to submit an Input DTO without specifying certain properties. Even if they want a property to be null, they need to explicitly specify it.

  • If the property of the Input DTO is null, the corresponding property of the obtained Jimmer entity object will also be set to null, effectively modifying the corresponding field in the database to null.

Two ways for the client to submit data:

  • Submit an Input DTO with the storeId property set to null

    curl -X 'PUT' \
    'http://localhost:8080/book' \
    -H 'accept: */*' \
    -H 'Content-Type: application/json' \
    -d '{
    "id": 12,
    "name": "TURING",
    "storeId": null
    }'

    The print result (the final Jimmer entity object obtained) is as follows:

    {
    "id":12,
    "name":"TURING",
    "store":null
    }

    That is, subsequent operations will update the corresponding field in the database to null.

  • Submit an Input DTO without the storeId property

    curl -X 'PUT' \
    'http://localhost:8080/book' \
    -H 'accept: */*' \
    -H 'Content-Type: application/json' \
    -d '{
    "id": 12,
    "name": "TURING"
    }'

    This request will be rejected, HTTP error code 400, parameter error. If you check the Java log, you will see the following error:

    Resolved [org.springframework.http.converter.HttpMessageNotReadableException:
    JSON parse error: Cannot construct instance of
    `org.doc.j.model.dto.BookUpdateInput$Builder`,
    problem: An object whose type is "org.doc.j.model.dto.BookUpdateInput"
    cannot be deserialized by Jackson.
    The current type is fixed input DTO so that
    all JSON properties must be specified explicitly,
    however, the property "storeId" is not specified by JSON explicitly.
    Please either explicitly specify the property as null in the JSON,
    or specify the current input properties as static, dynamic or fuzzy
    in the DTO language]
tip

If developer use the Automatic TypeScript Generation feature in Jimmer, the generated typeScript code will require web developers to provide the storeId property for the BookUpdateInput object, otherwise client code cannot be compiled successfully.

2. static

DTO code example:

input BookUpdateInput {
id!
name
static id(store)
}
  • When submitting an Input DTO, the user can either set the storeId property to null or not specify it at all.

  • Regardless of the user's choice, the behavior remains the same: the corresponding property of the obtained Jimmer entity object will definitely be set to null, effectively modifying the corresponding field in the database to null.

Two ways for the client to submit data:

  • Submit an Input DTO with the storeId property set to null

    curl -X 'PUT' \
    'http://localhost:8080/book' \
    -H 'accept: */*' \
    -H 'Content-Type: application/json' \
    -d '{
    "id": 12,
    "name": "TURING",
    "storeId": null
    }'

    The print result (the final Jimmer entity object obtained) is as follows:

    {
    "id":12,
    "name":"TURING",
    "store":null
    }

    That is, subsequent operations will update the corresponding field in the database to null.

  • Submit an Input DTO without the storeId property

    curl -X 'PUT' \
    'http://localhost:8080/book' \
    -H 'accept: */*' \
    -H 'Content-Type: application/json' \
    -d '{
    "id": 12,
    "name": "TURING"
    }'

    The print result (the final Jimmer entity object obtained) is as follows:

    {
    "id":12,
    "name":"TURING",
    "store":null
    }

    That is, subsequent operations will update the corresponding field in the database to null.

info

The effects of the two operations are the same, and the final effect is only affected by the DTO shape, regardless of whether the user specified the DTO property or not.

3. dynamic

DTO code example:

input BookUpdateInput {
id!
name
dynamic id(store)
}
  • If the user chooses to set the storeId property of the DTO to null, then the storeId property of the final obtained Jimmer entity object will also be null, effectively modifying the corresponding field in the database to null.

  • If the user does not set the storeId property of the DTO at all, then the storeId property of the final obtained Jimmer entity object will also not be set, so the corresponding field in the database will not be updated.

Two ways for the client to submit data:

  • Submit an Input DTO with the storeId property set to null

    curl -X 'PUT' \
    'http://localhost:8080/book' \
    -H 'accept: */*' \
    -H 'Content-Type: application/json' \
    -d '{
    "id": 12,
    "name": "TURING",
    "storeId": null
    }'

    The print result (the final Jimmer entity object obtained) is as follows:

    {
    "id":12,
    "name":"TURING",
    "store":null
    }

    That is, subsequent operations will update the corresponding field in the database to null.

  • Submit an Input DTO without the storeId property

    curl -X 'PUT' \
    'http://localhost:8080/book' \
    -H 'accept: */*' \
    -H 'Content-Type: application/json' \
    -d '{
    "id": 12,
    "name": "TURING"
    }'

    The print result (the final Jimmer entity object obtained) is as follows:

    {
    "id":12,
    "name":"TURING",
    // There is no storeId property here
    }

    That is, subsequent operations will not update the corresponding field in the database.

info

The two ways of submitting data correspond to two completely different behaviors, suitable for professional client teams to have flexible control over the service behavior.

4. fuzzy

warning

This mode sacrifices functionality for conservativeness and safety, and is the only mode with incomplete functionality.

DTO code example:

input BookUpdateInput {
id!
name
fuzzy id(store)
}
  • If the user sets the storeId property of the DTO object to a non-null value, then the storeId property of the final obtained Jimmer entity object will be set accordingly, effectively modifying the corresponding field in the database to the specified value.

  • Otherwise (whether the storeId property of the DTO object is set to null or not specified at all), the storeId property of the final obtained Jimmer entity object will not be set, so the corresponding field in the database will not be updated.

Three ways for the client to submit data:

  • Submit an Input DTO with the storeId property set to null

    curl -X 'PUT' \
    'http://localhost:8080/book' \
    -H 'accept: */*' \
    -H 'Content-Type: application/json' \
    -d '{
    "id": 12,
    "name": "TURING",
    "storeId": null
    }'

    The print result (the final Jimmer entity object obtained) is as follows:

    {
    "id":12,
    "name":"TURING",
    // There is no storeId property here
    }

    That is, subsequent operations will not update the corresponding field in the database.

  • Submit an Input DTO without the storeId property

    curl -X 'PUT' \
    'http://localhost:8080/book' \
    -H 'accept: */*' \
    -H 'Content-Type: application/json' \
    -d '{
    "id": 12,
    "name": "TURING"
    }'

    The print result (the final Jimmer entity object obtained) is as follows:

    {
    "id":12,
    "name":"TURING",
    // There is no storeId property here
    }

    That is, subsequent operations will not update the corresponding field in the database.

  • Submit an Input DTO with the storeId property set to a non-null value

    The previous two ways cannot modify the corresponding field in the database unless a non-null value is specified, as follows:

    curl -X 'PUT' \
    'http://localhost:8080/book' \
    -H 'accept: */*' \
    -H 'Content-Type: application/json' \
    -d '{
    "id": 12,
    "name": "TURING",
    "storeId": 2
    }'

    The print result (the final Jimmer entity object obtained) is as follows:

    {
    "id":12,
    "name":"TURING",
    "store":3
    }
info

This mode sacrifices the ability to modify the corresponding field in the database to null in exchange for absolute conservativeness and safety. It is particularly suitable for client teams with less experience.

Higher-Level Configurations

In the examples above, the keywords fixed, static, dynamic, and fuzzy were used to modify the nullable properties of the Input DTO.

Field-level control is the most fine-grained. However, if there are many nullable properties in the Input DTO, configuring them one by one may be cumbersome.

Jimmer provides configuration methods with a broader scope of influence:

  • Input type level

    dynamic input XxxInput {
    fixed nullableProp1
    static nullableProp2
    nullableProp3
    fuzzy nullableProp4
    nullableProp5
    }

    Here, the null handling mode is not declared for nullableProp3 and nullableProp5, and they will share the configuration at the input type level (in this case, dynamic).

  • Precompiler level

    If no configuration is found at the input type level, refer to the global configuration parameter jimmer.dto.defaultNullableInputModifier of the precompiler (for Java, it is APT; for Kotlin, it is KSP).

    <plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-compiler-plugin</artifactId>
    <version>3.13.0</version>
    <configuration>
    <compilerArgs>
    <arg>-Ajimmer.dto.defaultNullableInputModifier=fixed</arg>
    </compilerArgs>
    </configuration>
    </plugin>
  • Final default mode

    If no configuration is provided at the precompiler level either, the final default is static.

info

Configurations at different levels may conflict, and the priorities among them are:

Input property level config > Input type level config > Precompiler global config > Final default static

Caveats

caution

For the fixed and dynamic modes, Jimmer requires the server-side to use Jackson for deserialization.

Therefore, if you plan to use the fixed or dynamic mode, please:

  • Add @RequestBody

    If you carefully look at the examples in this article, you will notice that @RequestBody was used there.

  • Do not replace the Jackson Message Converter enabled by default in Spring Boot.

    In fact, not only the Input DTOs using the fixed or dynamic mode discussed in this article have this requirement; if the user needs to use the serialization/deserialization of Jimmer entities themselves, Jackson is also required.

    Jackson is carefully designed to strike the perfect balance between functionality and performance. Therefore, Jimmer regards Jackson as an essential infrastructure.