Which Jackson serialization/mapper features matter for building a stable, resilient JSON API, and how do you configure them globally vs per field?
answer
- FAIL_ON_UNKNOWN_PROPERTIES=false for forward-compat input
- default-property-inclusion=non_null (null vs absent semantics)
- FAIL_ON_EMPTY_BEANS=false avoids surprise 500s
- @JsonProperty/@JsonAlias decouple wire names from Java
- Naming strategy & field names = versioned contract
basics
~10 sSet FAIL_ON_UNKNOWN_PROPERTIES=false for forward-compatible input, choose a global default-property-inclusion (e.g. non_null), disable FAIL_ON_EMPTY_BEANS, register JavaTimeModule, and use annotations like @JsonInclude/@JsonFormat/@JsonProperty for per-field overrides.
solid answer
~40 sFor durable contracts I tune a few axes. Inbound resilience: spring.jackson.deserialization.fail-on-unknown-properties=false so adding fields on the client side doesn't break older servers (or keep it strict for internal APIs — a deliberate choice). Output shape: default-property-inclusion=non_null to drop nulls, but override per field with @JsonInclude when semantics differ. Robustness: disable SerializationFeature.FAIL_ON_EMPTY_BEANS to avoid crashes on beans without properties. Always register JavaTimeModule and keep WRITE_DATES_AS_TIMESTAMPS off for ISO dates. Naming is a contract: pick one property-naming-strategy and don't flip it later. Per field I lean on @JsonProperty (stable external names decoupled from Java names), @JsonIgnore/@JsonIgnoreProperties (hide internals), and @JsonInclude/@JsonFormat. The principle: global defaults for consistency, annotations for deliberate exceptions, and treat every serialized name/format as a versioned contract.
code
yaml · 16 linesspring:
jackson:
default-property-inclusion: non_null # slim payloads
property-naming-strategy: SNAKE_CASE # frozen contract
deserialization:
fail-on-unknown-properties: false # tolerate newer clients
read-unknown-enum-values-as-null: true
serialization:
fail-on-empty-beans: false # avoid surprise 500s
write-dates-as-timestamps: false # ISO-8601
---
# Per-field deliberate exceptions (Java):
# @JsonProperty("created_at") @JsonFormat(shape = STRING, pattern = "yyyy-MM-dd")
# private LocalDate createdAt;
# @JsonAlias({"emailAddress", "mail"}) private String email; // smooth rename
# @JsonInclude(Include.ALWAYS) private String note; // null is meaningfulgo deeper
Know a couple of toggles exist (ignore unknown fields, drop nulls) and that annotations override globally.
Configure the common features and per-field annotations; understand null-vs-absent basics.
Balance input tolerance vs strictness, choose inclusion/naming deliberately, and use aliases for safe evolution.
Treat JSON as a versioned contract: set org-wide defaults, enforce additive evolution, reason about determinism/caching, and codify tests that lock payload shape.
## Framing: JSON is a public contract Once clients depend on your JSON, field names, presence, types, and formats are a versioned API. Jackson config decides both robustness (does a payload change crash us?) and shape (what the contract looks like). ## Key feature axes ### Inbound tolerance — `DeserializationFeature` - `FAIL_ON_UNKNOWN_PROPERTIES` (Jackson default true): with an unknown JSON field, deserialization throws. For **public/evolving** APIs set `spring.jackson.deserialization.fail-on-unknown-properties=false` so newer clients can send extra fields without breaking older servers (Postel's law). For **internal, tightly-versioned** APIs, keeping it strict catches typos — a deliberate trade-off. Per class you can also use `@JsonIgnoreProperties(ignoreUnknown = true)`. - `READ_UNKNOWN_ENUM_VALUES_AS_NULL` / `...USING_DEFAULT_VALUE`: tolerate new enum values from clients instead of throwing. - `FAIL_ON_NULL_FOR_PRIMITIVES`: guard against nulls into primitives. ### Output shape — inclusion & naming - `spring.jackson.default-property-inclusion=non_null|non_empty|non_absent|always` (maps to `@JsonInclude`). `non_null` is a common default to slim payloads; be careful — omitting a field vs sending `null` can mean different things to clients (absence vs explicit clear). Override per field with `@JsonInclude(Include.ALWAYS)` where the null is semantically meaningful. - `property-naming-strategy` (SNAKE_CASE, LOWER_CAMEL_CASE, etc.): a global naming contract. Changing it later breaks every client — treat as immutable once shipped. ### Robustness — `SerializationFeature` - `FAIL_ON_EMPTY_BEANS` (default true): serializing an object with no detectable properties throws `InvalidDefinitionException`. Disabling (`spring.jackson.serialization.fail-on-empty-beans=false`) prevents surprise 500s, e.g. from proxies/lazy entities. - `WRITE_DATES_AS_TIMESTAMPS`: keep **off** for ISO-8601 (Boot default). - `WRITE_ENUMS_USING_TO_STRING` / index: controls enum output form — pick one and freeze it. - `ORDER_MAP_ENTRIES_BY_KEYS` / `MapperFeature.SORT_PROPERTIES_ALPHABETICALLY`: determinism helps diffing/caching but is a contract choice. - `INDENT_OUTPUT`: pretty-print (usually off in prod for size). ### Modules Register `JavaTimeModule` (Boot does this) and any domain `Module`s. Module registration order can matter when two modules provide serializers for the same type — later registration can win; keep it explicit. ## Per-field annotations (deliberate exceptions) - `@JsonProperty("external_name")`: decouple the wire name from the Java field — lets you rename Java code without breaking the contract. - `@JsonIgnore` / `@JsonIgnoreProperties`: hide internal fields; combine with `ignoreUnknown` for inbound tolerance. - `@JsonInclude`: per-field null/empty rules overriding the global default. - `@JsonFormat`: per-field date/number formatting. - `@JsonAlias`: accept multiple inbound names for one property (smooth renames). - `@JsonView`: per-endpoint field subsets. ## Global vs per-field philosophy Set **global defaults** for consistency (inclusion, naming, tolerance, ISO dates). Use **annotations** only for intentional deviations. Avoid scattering conflicting local overrides that make the contract unpredictable. ## Evolution & versioning gotchas - Never rename a serialized field in place — add `@JsonAlias`/new field, deprecate old. - Flipping `default-property-inclusion` or naming strategy is a breaking change for all clients. - Strict `FAIL_ON_UNKNOWN_PROPERTIES` on public APIs makes additive client changes break you — usually relax it. - Determinism (sorted keys) aids caching/ETags but is itself a contract. - Test with `@JsonTest` and serialize sample payloads in unit tests to lock the contract. ## When to use what - Public API: lenient input (`fail-on-unknown-properties=false`, `@JsonIgnoreProperties(ignoreUnknown=true)`), slim output (`non_null`), robust (`fail-on-empty-beans=false`), ISO dates, stable naming. - Internal/strict: keep failures on to catch mismatches early. - Always make naming/format changes additive and versioned.
- Why can default-property-inclusion=non_null be risky for a PATCH/update API?Because omitting a field and sending it as null can mean different things (leave unchanged vs clear it). If nulls are dropped, clients can't express 'set this to null'. Use @JsonInclude(ALWAYS) on such fields or JsonNullable/Optional patterns to distinguish absent from null.
- How do you rename a JSON field without breaking existing clients?Don't rename in place. Add the new name and keep accepting the old via @JsonAlias for input, emit both (or keep the old) for output during a deprecation window, then remove the old name in a new API version. Use @JsonProperty to decouple wire name from the Java field.
- What does FAIL_ON_EMPTY_BEANS guard against and why disable it?It throws when serializing an object Jackson finds no properties for (e.g. a Hibernate proxy or a class with only private fields and no getters/visibility). Disabling it prevents unexpected 500s, though the real fix is ensuring the type is actually serializable.
saying these in an interview costs you the question
- Renaming serialized fields in place without aliases/versioning
- Assuming non_null inclusion is always safe (breaks explicit-null PATCH semantics)
- Keeping FAIL_ON_UNKNOWN_PROPERTIES strict on a public evolving API by accident
- Treating property-naming-strategy as freely changeable after release