Series MapLesson 06 / 32
Start HereOrdered learning track

Learn Java Data Mapper Json Xml Validation Part 006 Null Absence Defaults Empty Values

17 min read3259 words
PrevNext
Lesson 0632 lesson track0106 Start Here

title: Learn Java Data Mapper, JSON/XML Processing & Validation - Part 006 description: Null, absence, defaults, empty values, and partial intent in Java data contracts, Jackson, MapStruct, and Jakarta Validation. series: learn-java-data-mapper-json-xml-validation seriesTitle: Learn Java Data Mapper, JSON/XML Processing & Validation order: 6 partTitle: Null, Absence, Defaults, Empty Values tags:

  • java
  • json
  • jackson
  • mapstruct
  • validation
  • null-handling
  • serialization
  • deserialization
  • dto date: 2026-06-29

Part 006 — Null, Absence, Defaults, Empty Values

Goal: mampu mendesain semantics untuk missing, explicit null, empty, default, dan concrete value sehingga mapper, validator, serializer, dan consumer tidak saling menebak.

Null adalah salah satu sumber bug paling mahal dalam data boundary. Masalahnya bukan hanya NullPointerException. Masalah sebenarnya adalah hilangnya intent.

Di boundary, nilai kosong bisa berarti banyak hal:

  • client lupa mengirim field;
  • client sengaja mengirim null untuk clear value;
  • client mengirim empty string karena UI form kosong;
  • serializer menghilangkan field karena NON_NULL;
  • mapper mengabaikan field karena patch semantics;
  • default Java mengisi false, 0, atau null;
  • database default mengisi nilai setelah insert;
  • validation memperlakukan blank berbeda dari null.

Jika sistem tidak membedakan kondisi ini, bug akan terlihat seperti “mapper issue”, padahal desain contract-nya ambigu.


1. Kaufman Framing: High-Value Subskill

Untuk menjadi efektif cepat, kuasai satu subskill ini:

Jangan pernah bertanya “field ini null atau tidak?” sebelum bertanya “null di boundary ini artinya apa?”

Kita akan membedakan lima state:

  1. Absent — property tidak ada di payload.
  2. Explicit null — property ada dan nilainya null.
  3. Empty — property ada tetapi kosong: "", [], {}.
  4. Default — nilai muncul karena aturan default, bukan input langsung.
  5. Concrete value — nilai eksplisit bermakna.

2. The Five-State Model

State ini harus diputuskan per boundary, bukan secara global.

StateJSON ExampleJava RepresentationPossible Meaning
Absent{}often null after bindingnot provided, preserve existing, use default, reject missing
Explicit null{ "name": null }nullclear, unknown, invalid, intentionally blank
Empty string{ "name": "" }""blank input, clear-like, invalid, meaningful empty label
Empty array{ "tags": [] }empty listclear all, no tags, valid empty set
Defaultno direct examplefalse, 0, configured valuesystem-assigned, Java primitive default, mapper default
Concrete{ "name": "Ayu" }"Ayu"set/update/value

3. Why Java Makes This Hard

Java object fields collapse many states.

public class UpdateCustomerRequest {
    private String name;
    private boolean active;
}

After deserialization:

Payloadnameactive
{}nullfalse
{ "name": null }nullfalse
{ "active": false }nullfalse
{ "active": true }nulltrue

For name, absent and explicit null collapsed. For active, absent and explicit false collapsed because primitive boolean defaults to false.

This is why patch DTOs with primitive fields are dangerous.


4. Create vs Update vs Patch Semantics

The correct null behavior depends on operation type.

4.1 Create

In create operations, absent and null are usually invalid for required fields.

{}

For required name, this should fail.

{ "name": null }

This should also fail.

{ "name": "" }

Usually fail if name must be non-blank.

Create DTO:

public record CreateCustomerRequest(
        @NotBlank String name,
        @NotBlank String email
) {
}

4.2 Replace

In replace operations, absent often means “set missing fields to default/empty” or “invalid because full representation required”.

Replace should be strict. A replace payload should usually represent the whole resource state.

4.3 Patch

In patch operations:

  • absent often means preserve existing value;
  • explicit null may mean clear existing value;
  • concrete value means set/update;
  • empty collection may mean clear all;
  • empty string may be invalid or may mean set to empty label depending on domain.

Plain Java nullable fields cannot represent this safely.


5. The Patch Intent Problem

Bad patch DTO:

public record PatchCustomerRequest(
        String displayName,
        String middleName,
        Boolean marketingOptIn
) {
}

This object cannot reliably answer:

  • Was middleName absent?
  • Was middleName explicitly set to null?
  • Did the client want to clear it?
  • Did the JSON parser default anything?

Better: model intent explicitly.

public sealed interface FieldPatch<T>
        permits FieldPatch.Absent, FieldPatch.Clear, FieldPatch.Set {

    record Absent<T>() implements FieldPatch<T> {
    }

    record Clear<T>() implements FieldPatch<T> {
    }

    record Set<T>(T value) implements FieldPatch<T> {
    }
}

Request:

public record PatchCustomerRequest(
        FieldPatch<String> displayName,
        FieldPatch<String> middleName,
        FieldPatch<Boolean> marketingOptIn
) {
}

Use-case logic:

public Customer patch(Customer current, PatchCustomerCommand command) {
    var next = current;

    next = switch (command.middleName()) {
        case FieldPatch.Absent<String> ignored -> next;
        case FieldPatch.Clear<String> ignored -> next.clearMiddleName();
        case FieldPatch.Set<String> set -> next.changeMiddleName(new CustomerName(set.value()));
    };

    return next;
}

This is more code, but the intent is explicit and testable.


6. Null Policy Matrix

Every boundary should define a null policy matrix.

BoundaryAbsentExplicit NullEmpty StringEmpty ArrayDefault
Create requestreject if requiredreject if requiredreject/normalizevalid if collection may be emptytechnical only
Patch requestpreserveclear or rejectset/reject/clearset empty collectionavoid hidden default
Responseomit or include by policyexpose if meaningfulexpose if meaningfulexpose empty list oftenexplicit contract
Eventprefer explicit stable fieldsavoid ambiguous nullsavoid blank if invalidstable semanticnever silent business default
XML documentschema-drivenschema nillable-drivenschema/domain-drivenschema/domain-drivenschema/default-driven
Internal commandno ambiguitymodel intentnormalizeddomain-specificexplicit

Without this matrix, teams argue about nulls repeatedly in code review.


7. Jackson Serialization: Output Null Policy

Jackson output inclusion controls whether fields appear in serialized JSON.

Common modes:

InclusionMeaningRisk
ALWAYSinclude property regardless of valuenoisy but explicit
NON_NULLomit Java null valuesabsent/null distinction lost for consumer
NON_ABSENTomit null and absent referential values such as empty Optionalcan hide intent if overused
NON_EMPTYomit null and empty strings/collections/arrayscan hide meaningful empty
NON_DEFAULTomit values considered defaultdangerous for compatibility unless carefully tested

Example:

@JsonInclude(JsonInclude.Include.NON_NULL)
public record CustomerResponse(
        String customerId,
        String displayName,
        String middleName
) {
}

If middleName == null, output omits the field.

{
  "customerId": "C-001",
  "displayName": "Ayu"
}

This may be fine for response readability. It may be bad for event contracts where consumer needs to know that middleName is unknown or intentionally absent.

Production Rule

Set inclusion policy per boundary, not by global habit.

Bad:

objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

This global setting silently changes every response/event/debug payload.

Better:

  • configure global defaults conservatively;
  • use boundary-specific DTO annotation;
  • test output shape;
  • avoid NON_EMPTY unless empty truly means “not present”.

8. Jackson Deserialization: Input Null Policy

Output null policy and input null policy are different.

For input, explicit null may be:

  • accepted as null;
  • skipped;
  • converted to empty;
  • rejected;
  • failed for primitives;
  • handled by custom deserializer.

Jackson has null-handling tools such as @JsonSetter and Nulls for explicit null handling.

Example:

public class CreateCustomerRequest {
    private String name;

    @JsonSetter(nulls = Nulls.FAIL)
    public void setName(String name) {
        this.name = name;
    }
}

This rejects explicit null for name at deserialization time.

However, be careful: validation error and deserialization error are different failure categories.

Failure TypeExampleTypical Owner
Deserialization errorinvalid JSON type, forbidden explicit null if configuredparser/binder
Validation errorblank name, missing required field after bindingvalidation layer
Business rule errorcustomer type cannot use productuse-case/domain

Do not push all validation into Jackson. Use Jackson for structural binding rules and Jakarta Validation for input constraints.


9. @NotNull, @NotEmpty, @NotBlank: Different Meanings

Jakarta Validation constraints are not interchangeable.

ConstraintRejects nullRejects empty stringRejects blank stringApplies To
@NotNullyesnonoany type
@NotEmptyyesyesnot necessarily whitespace-only depending type semanticsstring/collection/map/array
@NotBlankyesyesyescharacter sequence

Examples:

public record CreateUserRequest(
        @NotBlank String username,
        @NotEmpty List<@NotBlank String> roles,
        @NotNull Boolean termsAccepted
) {
}

Do not use @NotNull for a user-facing text field if blank text is invalid.

Bad:

public record CreateProductRequest(
        @NotNull String name
) {
}

This allows "" and potentially " ".

Better:

public record CreateProductRequest(
        @NotBlank String name
) {
}

Primitive Trap

public record CreateUserRequest(
        @NotNull boolean termsAccepted
) {
}

This is meaningless. Primitive boolean cannot be null. Use Boolean if you need to validate presence.

public record CreateUserRequest(
        @NotNull Boolean termsAccepted
) {
}

Then business rule may additionally require it to be true.


10. Empty Is Not Always Invalid

Empty collection often has legitimate meaning.

{
  "roles": []
}

This might mean:

  • user has no roles;
  • clear all roles;
  • invalid because at least one role required;
  • query filter matches no roles;
  • consumer should show empty state.

Use the correct constraint:

public record AssignRolesRequest(
        @NotEmpty List<@NotBlank String> roles
) {
}

But for a search filter:

public record SearchUsersRequest(
        List<String> roles
) {
}

An empty list may mean “filter by no roles”, “no filter”, or invalid. Decide explicitly. Do not let UI/library behavior define it accidentally.


11. Defaults: The Most Dangerous Convenience

Defaults come from many places.

Each default has an owner.

Default SourceExampleRisk
Java primitive defaultfalse, 0absent collapsed into value
Field initializerprivate int page = 0hides input absence
Constructorsize == null ? 50 : sizegood if technical, risky if business
Builderstatus = ACTIVEaccidental business decision
MapperdefaultValue = "ACTIVE"mapping layer makes policy
Databasedefault column valueobject state differs before/after save
Serializer omissionomit null/defaultconsumer cannot infer source

Production Rule

Default is acceptable only when all are true:

  1. owner is clear;
  2. default is documented in contract;
  3. default is tested;
  4. default does not hide user intent;
  5. default is safe under security/compliance failure mode.

12. MapStruct Null Handling

MapStruct has several null-related strategies. The two most commonly confused are:

  • NullValueCheckStrategy — when generated code checks source for null before mapping;
  • NullValuePropertyMappingStrategy — how null or not-present source properties affect existing target properties in update mapping.

12.1 Create Mapping

@Mapper
public interface CustomerMapper {
    CustomerView toView(Customer source);
}

If source field is null, target field may become null unless configured or custom mapping handles it.

12.2 Update Mapping

@Mapper
public interface CustomerPatchMapper {
    @BeanMapping(nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE)
    void apply(PatchCustomerDto source, @MappingTarget CustomerDraft target);
}

With IGNORE, null source property does not overwrite existing target property.

This is useful when null means “not provided”, but wrong when null means “clear”.

12.3 Null Strategy Must Match Intent

IntentStrategy
Null means preserve existingIGNORE in update mapping can fit
Null means clear existingdo not use IGNORE; map explicit clear
Null means defaultuse explicit default, document owner
Null means invalidvalidate/reject before mapping
Absent distinct from nullDTO must preserve presence info

MapStruct cannot recover intent if DTO collapsed absent and null before mapper sees it.


13. Jackson + MapStruct Failure Chain

A common failure chain:

But the client intended to clear middleName. The bug is not in MapStruct. The bug is in ambiguous contract semantics.

Fix by modeling intent:


14. Optional in DTOs

Optional is useful as a return type in Java APIs, but fields/components of type Optional<T> in DTOs require caution.

public record CustomerResponse(
        String customerId,
        Optional<String> middleName
) {
}

This can express absence in Java, and Jackson has inclusion modes such as NON_ABSENT. But DTOs with many Optional fields can become noisy and may not solve explicit null vs absent unless input handling is designed carefully.

Use Optional in DTO only if:

  • team agrees on serialization/deserialization rules;
  • ObjectMapper modules/config are consistent;
  • JSON samples clearly document output;
  • tests cover absent, explicit null, and present values.

For patch intent, prefer explicit patch wrapper over Optional<T> because Optional.empty() usually means absent, not clear.


15. Empty String Normalization

Many systems normalize blank text at the boundary.

public final class TextNormalizer {
    public static String blankToNull(String value) {
        if (value == null) {
            return null;
        }
        var stripped = value.strip();
        return stripped.isEmpty() ? null : stripped;
    }
}

This can be good for create request:

public record CreateCustomerCommand(CustomerName name) {
}

Mapper:

@Mapper
public interface CustomerCommandMapper {
    default CustomerName toCustomerName(String value) {
        var normalized = TextNormalizer.blankToNull(value);
        return normalized == null ? null : new CustomerName(normalized);
    }
}

But do not normalize blindly for all fields.

FieldBlank Handling
customer nameusually reject/normalize strip
free-form noteblank may be valid
passworddo not strip without explicit security decision
codestrip + uppercase may be valid
address line 2blank may mean absent
search queryblank may mean no filter

Normalization is business semantics, not mere utility code.


16. Null in XML

XML has its own null/absence semantics.

Examples:

Absent element:

<Customer>
  <CustomerId>C-001</CustomerId>
</Customer>

Empty element:

<Customer>
  <MiddleName></MiddleName>
</Customer>

Nil element:

<Customer xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <MiddleName xsi:nil="true" />
</Customer>

These are not the same. XML schema can define whether element is required, optional, nillable, or has default/fixed values.

Production advice:

  • do not assume JSON null rules apply to XML;
  • read the XSD contract;
  • test marshal and unmarshal for absent, empty, and nil;
  • avoid sharing one DTO for JSON and XML if semantics differ.

17. Validation Groups and Null Semantics

Create and patch often need different validation.

public interface CreateChecks {
}

public interface PatchChecks {
}

DTO:

public record CustomerRequest(
        @NotBlank(groups = CreateChecks.class)
        String name,

        @Email(groups = {CreateChecks.class, PatchChecks.class})
        String email
) {
}

This can work, but be careful. One DTO with many groups can become hard to reason about.

Prefer separate DTOs when operation semantics differ sharply:

public record CreateCustomerRequest(
        @NotBlank String name,
        @Email @NotBlank String email
) {
}

public record PatchCustomerRequest(
        FieldPatch<String> name,
        FieldPatch<String> email
) {
}

Validation groups are powerful, but separate shapes are often clearer.


18. Designing a Null Semantics Contract

Every endpoint/message/document should answer these questions.

18.1 Requiredness

  • Is the field required on create?
  • Is it required on replace?
  • Is it optional on patch?
  • Is it always present in response?
  • Is it always present in event?

18.2 Nullability

  • Can consumer send explicit null?
  • Does explicit null mean clear?
  • Does explicit null mean unknown?
  • Is explicit null rejected?
  • Does serializer ever emit null?

18.3 Emptiness

  • Is empty string valid?
  • Is blank string valid?
  • Is empty collection valid?
  • Does empty collection mean clear all?
  • Should empty be normalized to null?

18.4 Defaults

  • Who owns the default?
  • Is default visible to consumer?
  • Is default audited?
  • Can default change without breaking compatibility?
  • Is default safe under failure?

Document these decisions near the DTO or schema, then enforce with tests.


19. Example: Production-Grade Patch Design

19.1 External JSON Contract

{
  "displayName": "Ayu",
  "middleName": null,
  "marketingOptIn": false
}

Meaning:

  • displayName: set to Ayu;
  • middleName: clear;
  • marketingOptIn: set to false;
  • absent fields: preserve.

19.2 Internal Command

public record PatchCustomerCommand(
        FieldPatch<CustomerName> displayName,
        FieldPatch<CustomerName> middleName,
        FieldPatch<Boolean> marketingOptIn
) {
}

19.3 Use-Case Application

public Customer applyPatch(Customer customer, PatchCustomerCommand command) {
    var result = customer;

    result = applyDisplayName(result, command.displayName());
    result = applyMiddleName(result, command.middleName());
    result = applyMarketingOptIn(result, command.marketingOptIn());

    return result;
}

private Customer applyMiddleName(Customer customer, FieldPatch<CustomerName> patch) {
    return switch (patch) {
        case FieldPatch.Absent<CustomerName> ignored -> customer;
        case FieldPatch.Clear<CustomerName> ignored -> customer.clearMiddleName();
        case FieldPatch.Set<CustomerName> set -> customer.changeMiddleName(set.value());
    };
}

This is not accidental mapping. It is an explicit state transition.


20. Test Matrix for Null Semantics

For each field with nuanced semantics, test all states.

CasePayloadExpected DTO/CommandExpected Result
absent{}Absentpreserve
explicit null{ "middleName": null }Clearclear field
empty string{ "middleName": "" }reject or normalizedomain-specific
blank string{ "middleName": " " }reject or normalizedomain-specific
concrete{ "middleName": "Made" }Set(CustomerName)update

JUnit-style skeleton:

@ParameterizedTest
@MethodSource("patchCases")
void mapsPatchIntent(String json, ExpectedPatch expected) throws Exception {
    var request = objectMapper.readValue(json, PatchCustomerRequest.class);
    var command = mapper.toCommand(request);

    assertThat(command.middleName()).isEqualTo(expected.middleName());
}

Do not only test happy path.


21. Observability of Null Decisions

Null/default decisions should be visible enough to debug.

Useful logs/metrics at boundary:

  • validation error count by field and reason;
  • rejected explicit null count;
  • patch clear operation count;
  • default applied count for important technical defaults;
  • dead-letter count for missing required event fields;
  • contract compatibility failure count.

Avoid logging raw sensitive values. Log field names and reasons, not secrets.

Example structured error:

{
  "code": "VALIDATION_FAILED",
  "violations": [
    {
      "field": "displayName",
      "reason": "must not be blank"
    }
  ]
}

22. Anti-Patterns

22.1 Global NON_NULL Everywhere

Global null omission makes payloads pretty but can hide meaning.

Use it only if every boundary accepts that contract.

22.2 Boolean Without Requiredness Decision

public record ConsentRequest(Boolean accepted) {
}

This distinguishes null from false, but does not define what null means. Add validation or command semantics.

22.3 Empty String as Magic Clear

{ "middleName": "" }

Using empty string to mean clear is possible, but must be explicit. It often conflicts with normalization and UI behavior.

22.4 Mapper Defaults as Business Policy

@Mapping(target = "status", source = "status", defaultValue = "ACTIVE")

This may be fine for a technical projection. It is risky for business status. Prefer use-case policy.

22.5 Validation After Destructive Mapping

If mapper converts blank to null before validation, user gets misleading error or invalid value passes. Decide order clearly.


Policy 1 — Create DTOs

  • Required text: @NotBlank.
  • Required object: @NotNull + @Valid if nested.
  • Required collection with at least one element: @NotEmpty + element constraints.
  • Optional fields: wrapper type, not primitive.
  • Business defaults: not in DTO; decide in use-case.

Policy 2 — Response DTOs

  • Prefer stable field presence for public contracts.
  • Use NON_NULL only when omission is documented.
  • Prefer empty arrays over null arrays for list fields when consumer expects iterable.
  • Do not expose internal null uncertainty as if it were meaningful.

Policy 3 — Patch DTOs

  • Do not use plain nullable fields when clear/preserve distinction matters.
  • Use explicit field intent wrapper.
  • Test absent/null/empty/concrete.
  • Avoid primitive fields.

Policy 4 — Events

  • Avoid ambiguous nulls.
  • Prefer additive optional fields for evolution.
  • Never let serializer omission accidentally change event meaning.
  • Golden-file test event payloads.

Policy 5 — XML

  • Follow XSD required/nillable/default semantics.
  • Test absent, empty, and xsi:nil.
  • Use dedicated XML document classes if JSON semantics differ.

Initial DTO:

public record MarketingConsentRequest(boolean accepted) {
}

Payload:

{}

Java result:

accepted == false

The system interprets missing field as user refused consent. That may be legally and analytically wrong.

Better DTO:

public record MarketingConsentRequest(
        @NotNull Boolean accepted
) {
}

Now missing/explicit null fails validation.

If patch semantics are required:

public record PatchMarketingConsentRequest(
        FieldPatch<Boolean> accepted
) {
}

Now absent means preserve, false means explicitly opt out, and explicit null can be rejected or interpreted based on policy.


25. Mini Case Study: Empty List Bug

Request:

public record UpdateUserRolesRequest(
        List<String> roles
) {
}

Payload:

{ "roles": [] }

Possible interpretations:

  1. clear all roles;
  2. user must have at least one role, reject;
  3. no change because no roles provided;
  4. remove no roles.

Correct design if clear all is allowed:

public record ReplaceUserRolesRequest(
        @NotNull List<@NotBlank String> roles
) {
}

Here empty list explicitly means no roles if domain permits it.

Correct design if at least one role is required:

public record ReplaceUserRolesRequest(
        @NotEmpty List<@NotBlank String> roles
) {
}

Correct design if patching role field:

public record PatchUserRolesRequest(
        FieldPatch<List<String>> roles
) {
}

26. Part 006 Exercises

Exercise 1 — Null Semantics Matrix

Pick one create request, one patch request, one response, and one event from your system. For each nullable/optional field, fill this table:

FieldAbsentExplicit NullEmptyDefaultConcrete

If you cannot fill it, the contract is ambiguous.

Exercise 2 — Primitive Default Audit

Search DTOs for:

  • boolean;
  • int;
  • long;
  • double;
  • primitive arrays.

For each primitive field, ask: is default false or 0 a valid user input or an accidental value?

Exercise 3 — MapStruct Patch Review

Find every mapper using NullValuePropertyMappingStrategy.IGNORE. For each source DTO field, decide whether null really means preserve. If any field uses null to mean clear, the mapper is wrong.

Exercise 4 — Output Inclusion Test

For one response DTO and one event DTO, serialize examples with null/empty values. Decide whether omitted fields are safe for consumers.


27. Part 006 Checklist

You are done with this part when you can:

  • distinguish absent, explicit null, empty, default, and concrete value;
  • explain why Java primitives are dangerous in DTOs;
  • design create, replace, patch, response, event, and XML null policies differently;
  • use Jakarta Validation constraints with correct semantics;
  • understand Jackson output inclusion vs input null handling;
  • choose MapStruct null strategy based on business intent;
  • write tests for absent/null/empty/default behavior;
  • reject global null-handling rules that hide contract meaning.

References

Lesson Recap

You just completed lesson 06 in start here. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.