Jackson annotations are how you tell the mapper “the JSON here doesn’t match the Java here” — a different key name, a field that should never leave the object, a date that needs a specific pattern, a constructor instead of setters. Most real-world DTOs need two or three of them; very few need all of them. This page is the lookup you reach for when you know what you need Jackson to do but not which annotation does it.
Every example below is a real, compiled, executed program in this post’s companion
module — nothing here is a guess at what Jackson would do. Along the way this rewrite
closes two real gaps in the original guide: two annotations its own reference table listed but
never actually demonstrated (@JsonUnwrapped and @JsonAnyGetter /
@JsonAnySetter) now get real sections, and a duplicate row in that table is fixed.
Versions used in this post. Jackson 3.2.3 (tools.jackson.core:jackson-databind), the current GA release verified againstmaven-metadata.xmlon Maven Central ·jackson-annotations2.22 · JDK 21. Every@Json*annotation you will see below still comes fromcom.fasterxml.jackson.annotation—jackson-annotationsdeliberately keeps its Jackson-2-era group ID and package (confirmed against jackson-databind 3.2.3’s own POM, which still pins it there) so one copy of the annotations can sit on the classpath of Jackson-2 and Jackson-3 code at the same time. Every program below was compiled and run against this post’s companion repository.
Where an annotation actually plugs in
Every one of these annotations acts on one of three moments: building the JSON out of a Java object (serialisation), building the Java object from JSON (deserialisation), or both at once. Knowing which moment an annotation belongs to is most of the way to remembering what it does.
@JsonUnwrapped and @JsonAnyGetter/@JsonAnySetter sit a
little apart from this picture — they change the shape of the JSON object itself
(flattening it, or catching whatever does not have a field) rather than just renaming or
filtering individual keys. They get their own sections further down, with the same
real-output treatment as everything else.
@JsonProperty — Rename a Field in JSON
Use @JsonProperty when the JSON key has to differ from the Java field name
— most often because you are consuming a third-party API that uses snake_case while your
codebase uses camelCase. It works on both directions at once: the rename applies whether
Jackson is writing the object out or reading it back in.
public record OrderSummary(
@JsonProperty("order_id") Long orderId,
@JsonProperty("customer_name") String customerName) { }
part3annotations/D01RenameAndIgnore.java
rename : {"order_id":1001,"customer_name":"Alice"}
read back : OrderSummary[orderId=1001, customerName=Alice]
Full transcript: docs/output/D01RenameAndIgnore.txt
The read back line matters as much as the serialised JSON: Jackson accepted
order_id on the way in and populated the orderId component correctly
— the same annotation, the same rename, both directions, with no separate configuration
for reading versus writing.
- On a record,
@JsonPropertygoes on the constructor component as shown above — putting it on a getter instead works too, but only one annotated spot is needed per field; duplicating it in both places is harmless but redundant.
@JsonIgnore — Exclude a Field Entirely
Place @JsonIgnore on any field or accessor you never want in the JSON, in
either direction. This is the one most worth double-checking on a security-sensitive field,
because the bidirectional part is the half people forget.
public record UserAccount(
String username,
@JsonIgnore String passwordHash) { }
part3annotations/D01RenameAndIgnore.java
ignore : {"username":"alice"}
read back : passwordHash=null
Full transcript: docs/output/D01RenameAndIgnore.txt
The bidirectional half is the one that matters for security. The transcript above sends a JSON payload with an injectedpasswordHashfield into the mapper, and it comes outnull—@JsonIgnorerefuses to populate the field from incoming JSON, not just to emit it. A field you only excluded from the output side would still accept an attacker-supplied value on the way in.
- If you only want a field excluded from output while still accepting it on input (or vice versa),
@JsonProperty(access = Access.WRITE_ONLY)/READ_ONLYis the finer-grained tool —@JsonIgnorealone always blocks both.
@JsonInclude — Skip Null or Empty Fields
@JsonInclude controls whether null, empty, or default-valued fields get
written to JSON at all. Apply it at class level to cover every field, or on a single field to
be selective.
@JsonInclude(JsonInclude.Include.NON_NULL)
public record ProductDetails(String productName, String productDescription, Double discountRate) { }
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public record CompactProduct(String productName, String notes, List<String> tags) { }
part3annotations/D02InclusionAndFormat.java
NON_NULL : {"productName":"Keyboard"}
NON_EMPTY : {"productName":"Keyboard"}
Full transcript: docs/output/D02InclusionAndFormat.txt
Both records above are passed a null description and an empty tag list, and
neither key appears in the output — NON_NULL and NON_EMPTY each
did exactly what their name says, nothing more.
NON_DEFAULT— skips a field whose value equals the Java default (0,false,null); useful on primitives, whereNON_NULLcannot help because a primitive is never null.ALWAYS(the default when no annotation is present) — writes every field regardless of value.
@JsonFormat — Control Date and Number Formatting
@JsonFormat was close to mandatory on every date field in Jackson 2, where an
unannotated LocalDate serialised as a numeric array like [2026,4,9].
Jackson 3 changes the default: java.time support is built in and
ISO-8601 is the out-of-the-box behaviour, so an unannotated LocalDate already comes
out as "2026-04-09". You now reach for @JsonFormat only for a
genuinely non-standard pattern, or to force a number to serialise as a quoted string.
public record InvoiceRecord(
Long invoiceId,
LocalDate defaultDate, // no annotation at all
@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd/MM/yyyy")
LocalDate ukStyleDate, // non-standard pattern
@JsonFormat(shape = JsonFormat.Shape.STRING)
double totalAmount) { } // number written as a string
part3annotations/D02InclusionAndFormat.java
formats : {"invoiceId":500,"defaultDate":"2026-04-09","ukStyleDate":"09/04/2026","totalAmount":"199.99"}
Full transcript: docs/output/D02InclusionAndFormat.txt
The original version of this guide got this one backwards. It said Jackson writes an unannotatedLocalDateas a numeric array “by default”, without noting that only applied to Jackson 2. ThedefaultDatefield above carries no annotation at all and still prints as"2026-04-09"— proof the default flipped in Jackson 3, not a trick. See “What changed from the original guide” near the end of this post.
java.timeneeding no annotation at all is part of a bigger Jackson 3 simplification that also covers records andOptional— the full picture is in Part 2: Jackson with Java Records and Optionals.
@JsonAlias — Accept Multiple Input Names
When the same logical field arrives under different JSON keys depending on the caller
— a public API using q, an internal one using query —
@JsonAlias lets one field accept every variant on input. It is read-only: it has
no effect on what gets written out, which is always the field’s own name (or its
@JsonProperty rename, if it has one).
public record SearchQuery(
@JsonAlias({ "q", "query", "search_term" }) String searchKeyword) { }
part3annotations/D03AliasAndUnknownFields.java
alias {"q":"jackson"} -> jackson
alias {"query":"jackson"} -> jackson
alias {"search_term":"jackson"} -> jackson
Full transcript: docs/output/D03AliasAndUnknownFields.txt
All three JSON shapes resolve to the same Java field. This is the annotation to reach for when you are migrating a key name and need to accept both the old and new name during the transition, or when several upstream systems each picked their own name for the same concept.
@JsonIgnoreProperties — Tolerate Unknown Fields at Class Level
@JsonIgnoreProperties(ignoreUnknown = true) is the per-class escape hatch from
FAIL_ON_UNKNOWN_PROPERTIES. Worth knowing before you reach for it in
Jackson 3: that feature already defaults to off, so a stock
JsonMapper tolerates extra JSON fields with no annotation at all. The annotation now
earns its keep only on a mapper where strictness has been deliberately turned back on.
@JsonIgnoreProperties(ignoreUnknown = true)
public record OptedOutResponse(String status, String message) { }
part3annotations/D03AliasAndUnknownFields.java
default mapper : LenientResponse[status=OK, message=done]
strict mapper : UnrecognizedPropertyException (as expected)
strict + anno : OptedOutResponse[status=OK, message=done]
Full transcript: docs/output/D03AliasAndUnknownFields.txt
The same extra field (undocumentedField) is thrown at three mappers: the
default mapper shrugs it off with no annotation needed; a mapper with
FAIL_ON_UNKNOWN_PROPERTIES explicitly re-enabled throws
UnrecognizedPropertyException for a plain record; and that same strict mapper
still succeeds against OptedOutResponse, because the annotation opts that one
class back out.
- If you would rather keep the unknown data than discard it,
@JsonAnySetterbelow collects it into a map instead of silently dropping it.
@JsonCreator and @JsonProperty on Constructor Parameters
For an immutable class with no setters, Jackson needs to be told which constructor to call
and which JSON key maps to which parameter. @JsonCreator marks the constructor;
@JsonProperty on each parameter supplies the mapping — the same annotation
from the very first section, now doing its rename job on constructor parameters instead of
fields.
public static class ImmutablePoint {
private final double xCoordinate;
private final double yCoordinate;
@JsonCreator
public ImmutablePoint(@JsonProperty("x") double xCoordinate,
@JsonProperty("y") double yCoordinate) {
this.xCoordinate = xCoordinate;
this.yCoordinate = yCoordinate;
}
@JsonProperty("x") public double getXCoordinate() { return xCoordinate; }
@JsonProperty("y") public double getYCoordinate() { return yCoordinate; }
}
part3annotations/D04CreatorsAndUnwrapping.java
creator : ImmutablePoint(x=3.5, y=7.2)
round-trip: {"x":3.5,"y":7.2}
Full transcript: docs/output/D04CreatorsAndUnwrapping.txt
A record gets this for free — its canonical constructor is already
Jackson’s creator, and each component’s own name is already the implicit
@JsonProperty, which is why every earlier example on this page used a plain record
with no @JsonCreator in sight. Reach for an explicit @JsonCreator only
on a hand-written immutable class, a class with more than one constructor Jackson would
otherwise have to guess between, or a static factory method used in place of a constructor.
- How Jackson infers a creator without any annotation at all — single-constructor classes, in particular — is covered in Part 1: Jackson ObjectMapper Complete Guide.
@JsonUnwrapped — Flatten a Nested Object into Its Parent
Without this annotation, a field that is itself an object nests as its own
{...} inside the parent JSON. @JsonUnwrapped inlines that
nested object’s own fields directly into the parent’s JSON object instead, as if
the nested fields had been declared on the parent all along.
public record Address(String street, String city) { }
public static class Customer {
public String customerName;
@JsonUnwrapped public Address address;
}
part3annotations/D04CreatorsAndUnwrapping.java
unwrapped : {"street":"123 Main St","city":"Springfield","customerName":"Alice"}
Full transcript: docs/output/D04CreatorsAndUnwrapping.txt
street and city sit at the same level as customerName
— there is no "address":{...} wrapper anywhere in the output. This is the
annotation for when your domain model is composed of small value objects (an
Address, a Money) but the wire format you have to match is flat.
This annotation was listed in the original guide’s reference table and never once demonstrated in the post body. The code and the captured output above have existed in this post’s companion repository since it was first built — this rewrite is the first time they actually made it into the article. See “What changed from the original guide” near the end of this post.
- Two
@JsonUnwrappedfields whose nested objects share a key name collide, and Jackson throws at mapper-build time rather than silently picking one — worth knowing before you unwrap a second value object onto the same parent.
@JsonAnyGetter / @JsonAnySetter — Catch Whatever Does Not Have a Field
Every annotation so far assumes you know the complete set of JSON keys ahead of time.
@JsonAnySetter and @JsonAnyGetter are for when you do not: unmapped
properties land in a Map<String, Object> instead of being silently discarded
on the way in, or being left out entirely on the way out.
public static class FlexiblePayload {
public String knownField;
private final Map<String, Object> extras = new LinkedHashMap<>();
@JsonAnyGetter public Map<String, Object> extras() { return extras; }
@JsonAnySetter public void put(String k, Object v) { extras.put(k, v); }
}
part3annotations/D04CreatorsAndUnwrapping.java
any-setter: {surprise=1, another=[true, false]}
any-getter: {"knownField":"a","surprise":1,"another":[true,false]}
Full transcript: docs/output/D04CreatorsAndUnwrapping.txt
surprise and another were never declared as fields on
FlexiblePayload, yet both survive the round trip — in on
@JsonAnySetter, back out again on @JsonAnyGetter, sitting
alongside knownField in the final JSON as if they had always been real fields.
This pair was not in the original guide at all — not demonstrated, not mentioned, not in the reference table. It is the direct alternative to @JsonIgnoreProperties(ignoreUnknown = true) above: that annotation discards what it does not recognise, this pair keeps it.
- Combine this with
@JsonCreatorwhen the flexible payload also needs to be immutable — the constructor builds the known fields, and a@JsonAnySettermethod (not a field) still collects the rest during construction.
Three More That Belong to Other Posts in This Series
These three are real, show up in the reference table below, and are worth knowing exist — but each already has a full treatment, with its own real examples, in a different post in this series. Repeating them here would just be a worse copy.
@JsonTypeInfo and @JsonSubTypes — polymorphic deserialisation
When a field’s declared type is an abstract base class or interface and the JSON needs to say which concrete subtype each instance actually is, these two annotations add that type discriminator:
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "paymentType")
@JsonSubTypes({
@JsonSubTypes.Type(value = CreditCardPayment.class, name = "credit_card"),
@JsonSubTypes.Type(value = BankTransferPayment.class, name = "bank_transfer")
})
public abstract class PaymentMethod { /* ... */ }
part5polymorphic/PaymentMethod.java
The real failure mode worth knowing before you need it: serialising a plain
List<PaymentMethod> with writeValueAsString silently
drops the paymentType discriminator from every element, because
the mapper only sees List, not the concrete element type. The fix, and the exact
output showing both the broken and working versions, is in
Part 5: Polymorphic Deserialisation.
@JsonPropertyOrder — pin the field order
A record serialises in declaration order for free. A getter-based POJO does not — it serialises alphabetically, which is the usual reason a diff against a fixture file is not empty when the data is otherwise identical:
POJO : {"listPrice":79.99,"productId":1,"productName":"Mechanical Keyboard"}
record : {"productId":1,"productName":"Mechanical Keyboard","listPrice":79.99}
ordered : {"productId":1,"productName":"Mechanical Keyboard","listPrice":79.99}
Full transcript: part1objectmapper/docs/output/B04PropertyOrdering.txt, source: B04PropertyOrdering.java
@JsonPropertyOrder({"productId", "productName", "listPrice"}) pins the explicit
order shown in the third line above, which wins over both the record and POJO defaults.
Full write-up in Part 1: Jackson ObjectMapper Complete Guide.
Quick-Reference Table
Every annotation covered on this page, plus the three that belong to other posts in the series, in one lookup table.
| Annotation | Purpose | Applies To |
|---|---|---|
@JsonProperty | Rename a field in JSON (works both ways) | Field, method, constructor parameter |
@JsonIgnore | Exclude a field from JSON entirely, in both directions | Field, method |
@JsonInclude | Skip null / empty / default-valued fields | Class, field |
@JsonFormat | Format dates and numbers for a non-standard pattern | Field, method |
@JsonAlias | Accept multiple input key names for the same field | Field, parameter |
@JsonIgnoreProperties | Tolerate unknown fields at class level | Class |
@JsonCreator | Designate a constructor or factory for deserialisation | Constructor, static method |
@JsonUnwrapped | Flatten a nested object’s fields into its parent | Field |
@JsonAnyGetter / @JsonAnySetter | Collect unmapped properties into a Map instead of dropping them | Method |
@JsonTypeInfo | Add a type discriminator for polymorphism | Class |
@JsonSubTypes | Declare concrete subtypes for polymorphism | Class |
@JsonPropertyOrder | Pin output field order (getter-based POJOs are otherwise alphabetical) | Class |
When an Annotation Stops Being Enough
Should you reach for another annotation, or write a custom serialiser? If what you need can be stated as a rule about one field in isolation — rename it, skip it if null, format it this way — there is almost certainly an annotation above that already does it, and it is less code to maintain than a hand-written serialiser. Reach for a customValueSerializer/ValueDeserializerinstead once the logic needs to see multiple fields at once to decide what to write (aMoneytype that serialises differently depending on its own currency, say), or once you need to change how a third-party class you cannot annotate gets mapped, which is what Mix-ins are for. Stacking five annotations on a field to approximate logic a fifteen-line serialiser would express directly is the sign you have crossed that line.
Both of those — custom serialisers and Mix-ins for classes you cannot modify — get full real examples, including the Mix-in registration call, in Part 4: Custom Serialisers and Mix-ins.
AI Prompts for Jackson Annotations
Annotate a POJO to Match a JSON Schema
I have this Java POJO: [paste class here]. The JSON produced must match this target schema: [paste expected JSON here]. Apply the minimum set of Jackson 3 annotations needed — @JsonProperty, @JsonIgnore, @JsonInclude, @JsonFormat — to make the serialised output exactly match the target. Add a one-line comment above each annotation explaining why it is needed and what field-level problem it solves.
What it does: Analyses the gap between your POJO’s field names and the target JSON schema, then applies the precise annotation to each field that closes that gap. Each annotation is followed by an inline comment explaining the exact mapping it enforces, so future readers understand why each annotation is present.
When to use it: When integrating with an external API that uses a different naming convention (snake_case vs camelCase), requires specific fields to be omitted, or mandates a fixed date format. Run this prompt after you have defined your POJO and have the target JSON schema from the API documentation in hand.
Generate a Mix-in for a Third-Party Class
I need to control how this third-party class is serialised by Jackson 3 without modifying the class itself: [paste the class or its signature here]. Create a Mix-in annotation class that applies @JsonIgnore to fields I should not expose, renames fields using @JsonProperty, and formats any date fields with @JsonFormat. Then show how to register the Mix-in on a JsonMapper builder instance.
What it does: Produces an abstract Mix-in class whose annotations shadow
the target class’s fields, along with the
JsonMapper.builder().addMixIn(Target.class, TargetMixin.class).build()
registration call. The Mix-in is kept in a separate file so it can be updated independently if
the third-party class changes.
When to use it: When consuming a library whose classes you cannot modify — a generated SDK, or a domain model from another team’s module — but whose JSON representation needs renaming, filtering, or date formatting to match your API contract. A full worked Mix-in example lives in Part 4.
Standardise Date Annotation Formats
I have this Java class with multiple date fields using @JsonFormat: [paste class here]. Audit every @JsonFormat annotation and rewrite each one to use ISO-8601 format — yyyy-MM-dd for date-only fields and yyyy-MM-dd’T’HH:mm:ss for datetime fields. For each change, state the original pattern, the replacement pattern, and a one-sentence reason for the change. Flag any field where the original format carried business-specific meaning that ISO-8601 cannot preserve.
What it does: Audits every @JsonFormat(pattern=...) annotation
in the provided class, rewrites each to the appropriate ISO-8601 variant, and flags any edge
cases where the original pattern encoded domain-specific formatting that a standard pattern
cannot replicate — two-digit year formats or locale-specific separators, for example.
When to use it: When running a consistency audit on a codebase that has grown over several years with mixed date formats added by different developers. Also useful before exposing an API to external consumers who expect standard ISO-8601 dates rather than locale-specific or custom patterns. Remember that, per the @JsonFormat section above, Jackson 3 needs this annotation at all only for patterns that are not already ISO-8601.
What changed from the original guide
Rebuilding this page against a current Jackson GA, rather than trusting the original prose, surfaced the following:
- The quick-reference table listed
@JsonAliastwice, with two slightly different descriptions, and never listed@JsonAnyGetter/@JsonAnySetter‘s partner behaviour clearly. The table above has one row per annotation. @JsonUnwrappedwas listed in that table but never demonstrated anywhere in the post body, despite the companion repository already containing a real, passing example for it. It now has its own section above.@JsonAnyGetter/@JsonAnySetterwere not mentioned anywhere in the original post at all, despite — again — already having a real example sitting in the companion repository. They now have their own section above.- The companion module was bumped from Jackson 3.2.1 to the current GA, 3.2.3
— verified against
maven-metadata.xmlon Maven Central. Every one of the 36 programs across this repository’s eight parts produces byte-identical output on 3.2.3, confirming the bump changes nothing this series has claimed. - The original “Without
@JsonFormat, Jackson writesLocalDateas a numeric array by default” claim is corrected to note that only applied to Jackson 2 — see the@JsonFormatsection above.
Further reading
- jackson3-by-example companion repository — the full eight-part series, source, and every captured transcript
- Jackson 101: The Complete Java JSON Tutorial — if ObjectMapper basics themselves are new, start here
- Part 1: Jackson ObjectMapper Complete Guide — property ordering, creator inference, shared mapper configuration
- Part 2: Jackson with Java Records and Optionals — the broader Jackson 3 simplification referenced in the @JsonFormat section, covering records, Optional and java.time together
- Part 4: Custom Serialisers and Mix-ins — for when an annotation stops being enough
- Part 5: Polymorphic Deserialisation — the full @JsonTypeInfo / @JsonSubTypes treatment, including the List-serialisation failure mode
- jackson2-to-3-migration — the coexistence module proving Jackson 2 and Jackson 3 can share one copy of jackson-annotations on the same classpath
- jackson-annotations Javadoc, the primary reference this page samples from
Conclusion
Jackson’s annotation set is compact but powerful, and for the large majority of
projects, @JsonProperty, @JsonIgnore, @JsonInclude,
@JsonFormat, @JsonAlias, and @JsonCreator will cover
every mapping problem you run into. @JsonUnwrapped and
@JsonAnyGetter/@JsonAnySetter handle the two shape-changing cases
that come up less often but are easy to reach for once you know they exist. Keep this
quick-reference table bookmarked, and reach for a custom serialiser or Mix-in — covered
in Part 4 — only once a field’s mapping needs more context than annotations alone
can express.
No Comments yet!