2025-12-25
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
BareRequestis ineffective forBareRequest.Condition -
fromSourcebelongs toCondition, 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
Solution 1 (Most Recommended): Add the annotation to the nested record as well
@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": nullas 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
VI. An Easily Overlooked Related Issue (Important)
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)
-
@JsonInclude(NON_NULL)does not automatically apply to nested records -
List elements, inner records, and child objects all need separate declarations
-
Feign is just the “porter”; the real rules are in Jackson
-
FULL logs = the most truthful request body
-
Be cautious about the “pseudo-array string” issue with
Object value
Recommended Practices (Engineering Grade)
-
DTO / request body records
- Explicitly add
@JsonIncludeat both the top level and nested levels
- Explicitly add
-
Feign debugging phase
- Use FULL
-
Production
- Downgrade to BASIC / HEADERS
-
Query DSL
- Keep the
valuetype consistent with JSON semantics (String ≠ List)
- Keep the
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:
-
recorddegrading to reflection-basedset final fieldduring 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-dependenciesBOM explicitly manages the Jackson version (2.13.5) -
The
dependency-managementplugin 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
-
excludecan 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
-
enforcedPlatformbelongs to Gradle’s dependency resolution layer -
dependency-managementis 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:
-
Unlock version locking
-
Ensure the artifacts exist in the private repo
-
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
recordas Feign DTOs in the current project -
Use regular POJOs (without
finalfields) 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
- override Jackson module versions one by one in
-
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)
-
Enterprise BOM + dependency-management = ironclad version law
-
If you want to upgrade core libraries, you must first confirm:
-
BOM
-
private repository availability
-
-
For foundational libraries like Jackson / Netty / Log4j:
- “Can you fetch it?” matters more than “Is it new?”
-
Spring Boot 2.7 + Java 21 + record:
- is a “barely usable, but poorly matched” combination