Skip to main content

Jackson Annotations Cheat Sheet: Every Annotation You Need With Examples

Every Jackson 3 annotation worth knowing, in one lookup page, each backed by a real compiled and executed example: @JsonProperty through @JsonCreator, plus @JsonUnwrapped and @JsonAnyGetter/@JsonAnySetter, which the original guide listed but never actually demonstrated.

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 against maven-metadata.xml on Maven Central · jackson-annotations 2.22 · JDK 21. Every @Json* annotation you will see below still comes from com.fasterxml.jackson.annotation — jackson-annotations deliberately 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.

One annotation set, two directions Java object JSON serialize @JsonProperty · @JsonIgnore · @JsonInclude · @JsonFormat · @JsonAnyGetter deserialize @JsonProperty · @JsonAlias · @JsonCreator · @JsonIgnoreProperties · @JsonAnySetter @JsonProperty is the one annotation on both arrows — the same rename applies whichever direction the mapper is going, which is exactly why it is the first stop below.

@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, @JsonProperty goes 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 injected passwordHash field into the mapper, and it comes out null — @JsonIgnore refuses 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_ONLY is the finer-grained tool — @JsonIgnore alone 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, where NON_NULL cannot 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 unannotated LocalDate as a numeric array “by default”, without noting that only applied to Jackson 2. The defaultDate field 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.

@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, @JsonAnySetter below 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.

@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 @JsonUnwrapped fields 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 @JsonCreator when the flexible payload also needs to be immutable — the constructor builds the known fields, and a @JsonAnySetter method (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.

AnnotationPurposeApplies To
@JsonPropertyRename a field in JSON (works both ways)Field, method, constructor parameter
@JsonIgnoreExclude a field from JSON entirely, in both directionsField, method
@JsonIncludeSkip null / empty / default-valued fieldsClass, field
@JsonFormatFormat dates and numbers for a non-standard patternField, method
@JsonAliasAccept multiple input key names for the same fieldField, parameter
@JsonIgnorePropertiesTolerate unknown fields at class levelClass
@JsonCreatorDesignate a constructor or factory for deserialisationConstructor, static method
@JsonUnwrappedFlatten a nested object’s fields into its parentField
@JsonAnyGetter / @JsonAnySetterCollect unmapped properties into a Map instead of dropping themMethod
@JsonTypeInfoAdd a type discriminator for polymorphismClass
@JsonSubTypesDeclare concrete subtypes for polymorphismClass
@JsonPropertyOrderPin 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 custom ValueSerializer/ValueDeserializer instead once the logic needs to see multiple fields at once to decide what to write (a Money type 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 @JsonAlias twice, with two slightly different descriptions, and never listed @JsonAnyGetter / @JsonAnySetter‘s partner behaviour clearly. The table above has one row per annotation.
  • @JsonUnwrapped was 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 / @JsonAnySetter were 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.xml on 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 writes LocalDate as a numeric array by default” claim is corrected to note that only applied to Jackson 2 — see the @JsonFormat section above.

Further reading

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!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.