Skip to main content

2025-12-25

A Word

Life goes on endlessly from generation to generation, yet the river moon remains only similar year after year. --- “Spring River, Flower, Moon, Night” · Tang Dynasty · Zhang Ruoxu

Details

Full Text 春江潮水连海平,海上明月共潮生。
滟滟随波千万里,何处春江无月明!
江流宛转绕芳甸,月照花林皆似霰。
空里流霜不觉飞,汀上白沙看不见。
江天一色无纤尘,皎皎空中孤月轮。
江畔何人初见月,江月何年初照人?
人生代代无穷已,江月年年只相似。
不知江月待何人,但见长江送流水。
白云一片去悠悠,青枫浦上不胜愁。
谁家今夜扁舟子,何处相思明月楼?
可怜楼上月徘徊,应照离人妆镜台。
玉户帘中卷不去,捣衣砧上拂还来。
此时相望不相闻,愿逐月华流照君。
鸿雁长飞光不度,鱼龙潜跃水成文。
昨夜闲潭梦落花,可怜春半不还家。
江水流春去欲尽,江潭落月复西斜。
斜月沉沉藏海雾,碣石潇湘无限路。
不知乘月几人归,落月摇情满江树。


Feign + Jackson + Java Record

Analysis and Solution for the Issue Where null Fields Are Not Omitted During JSON Serialization


I. Background

When using a Java record as the Feign request body and using Jackson for JSON serialization, we expect to achieve:

When a field in the record is null, do not write that field into the JSON

Known conditions:

  • Using @JsonInclude(JsonInclude.Include.NON_NULL)

  • Feign Client sends the request

  • There is a nested record inside the record (BareRequest.Condition)

But in the Feign DEBUG logs we still see:

"fromSource": null

II. Reproduction Code (Simplified)

1️⃣ Record definition

@JsonInclude(JsonInclude.Include.NON_NULL)
public record BareRequest(
int mode,
String className,
boolean queryLike,
List<Condition> conditions
) {
public record Condition(
String column,
int option,
Object value,
Integer fromSource
) {}
}

2️⃣ Construct the request object

var conditions = List.of(
new BareRequest.Condition("cmdb_approval_status", 2, "YES", null),
new BareRequest.Condition("operational_status", 7, "[Decommissioned]", 1),
new BareRequest.Condition("server_type", 2, "PM-BareMetal", null),
new BareRequest.Condition("support_group", 2, "XPO-", null)
);

var request = new BareRequest(0, "All", true, conditions);
client.search(1, 3000, request);

3️⃣ Actual serialized log (problem symptom)

{
"column": "cmdb_approval_status",
"option": 2,
"value": "YES",
"fromSource": null
}

III. Root Cause Analysis (Key Conclusion)

✅ Conclusion 1: @JsonInclude does not propagate to nested types

  • @JsonInclude(NON_NULL) only applies to the type where it is declared

  • It does not automatically apply to:

    • inner records

    • element types in a List

    • child objects

Therefore:

  • The annotation on BareRequest is ineffective for BareRequest.Condition

  • fromSource belongs to Condition, so it is still serialized by default rules

This is Jackson’s established behavior, not a Feign issue, and not a record issue.


IV. Correct Solutions

@JsonInclude(JsonInclude.Include.NON_NULL)
public record BareRequest(
int mode,
String className,
boolean queryLike,
List<Condition> conditions
) {

@JsonInclude(JsonInclude.Include.NON_NULL)
public record Condition(
String column,
int option,
Object value,
Integer fromSource
) {}
}

✅ Result: When fromSource == null, the field will not appear in the JSON.


Solution 2 (More Precise): Control only a single field

public record Condition(
String column,
int option,
Object value,
@JsonInclude(JsonInclude.Include.NON_NULL)
Integer fromSource
) {}

Applicable scenarios:

  • You only want to omit one nullable field

  • Other fields should keep default behavior


Solution 3 (Global Solution, Use with Caution)

spring:
jackson:
default-property-inclusion: non_null

⚠️ Notes:

  • It affects the JSON serialization behavior of the entire application

  • Some APIs may rely on "field": null as meaningful semantics

  • Generally not recommended to enable globally by default


V. Why Feign Logs “Expose This Issue”

When Feign is set to loggerLevel = FULL:

  • It uses the final ObjectMapper serialization result

  • It performs no secondary processing

  • The log content = the actual HTTP body being sent

Therefore:

Feign logs are the most reliable evidence for whether Jackson configuration has taken effect


In the current logs, there is one condition:

"value": "[Decommissioned]"

In JSON semantics, this is:

  • ❌ a string

  • ❌ not an array

If the backend expects:

"value": ["Decommissioned"]

Then the correct Java construction should be:

new BareRequest.Condition(
"operational_status",
7,
List.of("Decommissioned"),
1
);

Otherwise it may cause:

  • the condition to become ineffective

  • it to be treated as a normal string match

  • abnormal query results

This issue is unrelated to null omission, but it can be fatal in DSL / query scenarios.


VII. Final Summary (Rules You Can Memorize)

  1. @JsonInclude(NON_NULL) does not automatically apply to nested records

  2. List elements, inner records, and child objects all need separate declarations

  3. Feign is just the “porter”; the real rules are in Jackson

  4. FULL logs = the most truthful request body

  5. Be cautious about the “pseudo-array string” issue with Object value


  • DTO / request body records

    • Explicitly add @JsonInclude at both the top level and nested levels
  • Feign debugging phase

    • Use FULL
  • Production

    • Downgrade to BASIC / HEADERS
  • Query DSL

    • Keep the value type consistent with JSON semantics (String ≠ List)

These conclusions are stably valid in Spring Boot 3.x + Java 21 + OpenFeign + Jackson environments.


Study Notes on Jackson Upgrade Failure (Gradle + Spring Boot 2.7 + Enterprise BOM Scenario)

I. Background

Project environment and characteristics:

  • Gradle multi-module project

  • Spring Boot 2.7.10

  • Java 21

  • Uses io.spring.dependency-management plugin

  • Imports enterprise internal BOM: com.xxx.xx:xx-dependencies

  • Needs to upgrade Jackson to resolve:

    • record degrading to reflection-based set final field during Feign / Jackson deserialization

    • resulting in IllegalAccessException

Goal:

  • Upgrade Jackson from 2.13.5 to 2.20.1

  • Ensure:

    • dependency resolution succeeds

    • build, codegen, and runtime all work


II. Symptoms and Core Errors

1. Jackson version remains locked at 2.13.5

Even after importing in the project:

mavenBom "com.fasterxml.jackson:jackson-bom:2.20.1"

dependencyInsight still shows:

com.fasterxml.jackson.core:jackson-databind:2.13.5
Selection reasons:
- Selected by rule
- By constraint

This indicates:

  • The Jackson version is not determined by ordinary dependency conflict resolution

  • It is forcibly locked by dependency-management’s BOM / constraints


2. Build fails after attempting the upgrade

After successfully “unlocking version management” and pointing dependencies to 2.20.1, the build fails during the codegen phase:

Could not find com.fasterxml.jackson.core:jackson-annotations:2.20.1

Key information:

  • The error comes from compileCodegenJava

  • The dependency chain clearly points to:

    • jackson-datatype-jsr310:2.20.1

    • jackson-bom:2.20.1

  • The repository cannot resolve jackson-annotations:2.20.1


III. Root Cause Analysis (Layered)

Layer 1: Why is Jackson always 2.13.5?

Because the version is locked at the dependency management layer, not because some library “happened to pull it in.”

Key configuration:

apply plugin: 'io.spring.dependency-management'

dependencyManagement {
imports {
mavenBom "com.xxx.xx:xx-dependencies:${version}"
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}

Conclusion:

  • The xx-dependencies BOM explicitly manages the Jackson version (2.13.5)

  • The dependency-management plugin generates constraints / rules

  • Ordinary mechanisms such as:

    • implementation(...)

    • platform(...)

    • enforcedPlatform(...)

    cannot override this level of locking


Layer 2: Why doesn’t importing jackson-bom:2.20.1 take effect?

Even if the import order is later:

mavenBom "com.fasterxml.jackson:jackson-bom:2.20.1"

It is still ineffective, which indicates:

  • The xframe BOM might be using:

    • dependencyManagement.dependencies

    • or version rules injected by internal plugins

  • The Jackson version is enforced by hard constraints, beyond what a normal BOM can override


Layer 3: Why does upgrading to 2.20.1 immediately report “artifact not found”?

The essence of the error is not a version conflict, but:

The repository does not contain / does not allow fetching jackson-annotations:2.20.1

Project repository configuration:

repositories {
mavenLocal()
mavenCentral()
}

In an enterprise environment:

  • Maven Central is often proxied by a private Nexus

  • New versions must be:

    • already synced into the private repo

    • or included in an allowlist

  • Otherwise, even if mavenCentral() is declared, it may not be truly reachable

Conclusion:

  • Jackson 2.20.1 is not available in the current private repository

  • Gradle cannot download jackson-annotations:2.20.1

  • Therefore the build fails


IV. Key Conclusions (Important)

Conclusion 1: You cannot solve Jackson version locking with exclude

  • exclude can only remove transitive dependencies

  • It cannot remove:

    • constraints generated by dependency-management

    • version rules injected by plugins

  • In this project: ineffective


Conclusion 2: enforcedPlatform does not work against dependency-management

  • enforcedPlatform belongs to Gradle’s dependency resolution layer

  • dependency-management is a higher-priority version management layer

  • When they conflict:

    • dependency-management wins

Conclusion 3: Upgrading Jackson ≠ just changing a version number

Upgrading Jackson in an enterprise project must satisfy all of the following:

  1. Unlock version locking

  2. Ensure the artifacts exist in the private repo

  3. Ensure all submodules / codegen / runtime can resolve them

None can be missing.


V. Practical Paths Forward

Path A (Most Stable, Short-Term Recommendation)

  • Continue using Jackson 2.13.5

  • Give up using record as Feign DTOs in the current project

  • Use regular POJOs (without final fields) instead

Pros:

  • No changes to infrastructure

  • Build and runtime remain stable

  • Matches the current xx + Boot 2.7 ecosystem


Path B (Mid-Term Compromise)

  • Confirm with the infrastructure team:

    • whether the private repo already has Jackson 2.18.x (LTS)
  • If available:

    • override Jackson module versions one by one in dependencyManagement.dependencies
  • Avoid very new versions like 2.20.x


Path C (Long-Term Cure)

  • Upgrade to Spring Boot 3.x

  • Upgrade accordingly:

    • Spring Cloud

    • xx dependencies

  • Use:

    • Jackson ≥ 2.15

    • mature support for record + constructor binding


VI. Final Conclusion About the Original record Deserialization Exception

The earliest error you encountered:

Can not set final boolean field ... stopDelivery

The final root cause is not:

  • boolean vs Boolean

  • JsonNaming

  • Feign configuration

But rather:

Jackson 2.13.5 in this combination failed to correctly use record constructor binding, degrading to reflective field writes

This is an ecosystem/version matching issue, not a coding-style issue.


VII. Engineering Lessons (Easy to Remember)

  1. Enterprise BOM + dependency-management = ironclad version law

  2. If you want to upgrade core libraries, you must first confirm:

    • BOM

    • private repository availability

  3. For foundational libraries like Jackson / Netty / Log4j:

    • “Can you fetch it?” matters more than “Is it new?”
  4. Spring Boot 2.7 + Java 21 + record:

    • is a “barely usable, but poorly matched” combination