JSON & Jackson
JSON syntax, the Jackson ObjectMapper, serialization & deserialization, annotations (@JsonProperty, @JsonIgnore), and working with generic types.
Assumes you know
After this you should be able to
- Explain the core idea in JSON & Jackson
- Apply JSON & Jackson in a representative exercise
My working notes on getting data out of Java objects and onto the wire, then back again.
Serialization, the JSON format, and the Jackson library (ObjectMapper + annotations).
I. Notebook Summary (copy by hand)#
Telegraphic, high-density. Keyword → rule. Everything here is derivable from Section II.
═══ PERSISTENCE & SERIALIZATION ═══ Collections data → lives in RAM → gone when program ends Persist data → write to file / DB / network Cross-language → need a NEUTRAL text format (not Java bytes) Serialization = Object → portable format (string/bytes) [encode] Deserialization = format → Object [decode] mnemonic: SERIAL = SEND (out) ; DE = DEcode back in
═══ DATA FORMATS ═══ JSON → most common on internet, human-readable, microservices' default XML → older, verbose, tag-based [+] CSV → flat tabular, no nesting [+] Binary: Avro / Parquet / ORC / Protobuf → compact, fast, NOT human-readable
═══ JSON SYNTAX ═══ object { } unordered set of "name": value pairs pair "name" : value pairs separated by , string "in double quotes" (keys ALWAYS strings in dq) number 42 25.7 -3e10 (no quotes; int OR decimal) boolean true | false (lowercase) null empty value for ANY type array [ ] ordered list of values nesting objects/arrays inside values = allowed [+] NO trailing comma. NO comments. NO single quotes. Keys must be strings.
═══ JACKSON CORE ═══ Lib = Jackson ; the engine = ObjectMapper (om) POJO = Plain Old Java Object (plain fields, no framework ties) [+] SERIALIZE om.writeValue(file, obj) obj → JSON file om.writeValueAsString(obj) obj → JSON String [+] DESERIALIZE om.readValue(json, Foo.class) JSON → Foo object How map happens: by FIELD NAME (key "age" → field age) match needs: matching name + getter/setter OR public field [+]
writeValue vs readValue#
| Method | Direction | In → Out |
|---|---|---|
writeValue(out, obj) | serialize | Object → JSON (to file/stream) |
writeValueAsString(obj) + | serialize | Object → JSON String |
readValue(src, Cls.class) | deserialize | JSON → Object |
═══ MISMATCH / EDGE BEHAVIOR ═══ field renamed (symbol→symbl) → deserialize: field stays NULL/default serialize: key uses NEW java name type mismatch (int→String) → Jackson coerces if it can; else throws (e.g. "100"→int OK; "abc"→int FAILS) [+] duplicate keys in JSON → LAST one wins (no error by default) [+] extra/unknown field in JSON → THROWS UnrecognizedPropertyException
═══ ANNOTATIONS (fix the mismatches) ═══ @JsonProperty("json_key") on field → map field <-> that exact key (decouple Java name from JSON name) @JsonIgnoreProperties(ignoreUnknown=true) on CLASS → silently skip unknown JSON fields @JsonIgnore [+] on field → never (de)serialize this one field
Annotation quick-pick#
| Problem | Annotation | Goes on |
|---|---|---|
| JSON key ≠ Java field name | @JsonProperty | field/getter |
| JSON has extra unknown keys | @JsonIgnoreProperties(ignoreUnknown=true) | class |
| Hide one field both ways + | @JsonIgnore | field |
═══ MENTAL MODEL ═══
[Java world] <--ObjectMapper--> [JSON text world]
objects gears/engine portable strings
field names annotations fix json keys
the name gapsII. Detailed Study Notes#
One numbered concept each: claim → plain-English build-up → why it matters → traps. + marks my additions and corrections.
1. Persistence: why in-memory data disappears#
Data stored in Java collections lives only in RAM, so it vanishes the moment the program stops; to keep it, you must write it somewhere durable.
When I put objects into an ArrayList, HashMap, or any collection, those objects
sit in the JVM's heap, which is just a region of RAM. RAM is volatile: cut the power (or end the
process) and it's wiped. So once main() returns, every collection and everything in it is
gone.
Persistence means surviving beyond a single program run. To persist data I have to move it out of RAM into something non-volatile: a file on disk, a database, or send it across the network to another machine that stores it. + The technical distinction is volatile (RAM, registers, cache) vs non-volatile (disk, SSD, DB) storage.
2. Why persisted data must be language-neutral#
If you want a program in any language to read your saved data, you can't save raw Java objects, you save a neutral, text-based format.
A Java object's in-memory layout is specific to the JVM. A Python or Go program has no idea what a Java
HashMap's bytes mean. So when data needs to cross language (or machine) boundaries, we
agree on a common, portable format, usually plain text with a documented structure. Both sides
know the format's rules, so both can read and write it.
This is exactly why data on the internet travels as text formats like JSON rather than as Java/Python objects: the receiver might be written in a totally different language.
Serializable produces JVM-specific binary that only Java can read,
and it's brittle across versions. JSON is language-agnostic and human-readable. +3. Serialization & Deserialization#
Serialization converts an in-memory object into a portable format; deserialization rebuilds the object from that format. They're encode/decode for data.
Serialization = take a live object and turn it into a flat sequence (a string or byte stream) you can store or send. Deserialization = take that flat sequence and reconstruct the equivalent object in memory. The pair is sometimes written (de)serialization.
The same idea shows up everywhere, even YouTube: video is encoded into data packets to transmit, then a decoder on your device turns packets back into a playable stream. Encoding/decoding is the general name; serialization/deserialization is that idea applied to program objects. + "Marshalling / unmarshalling" is a near-synonym you'll see in other ecosystems.
→ See code: serialize/deserialize round-trip
4. Data file formats: JSON and the field#
JSON is the dominant human-readable format for internet data exchange; binary formats exist for speed and size but you can't read them by eye.
JSON (JavaScript Object Notation) is the most common format for transferring data over the internet. It's plain text, so a human can open and read it, and microservices overwhelmingly use it to exchange information. Beyond JSON there's a whole family:
| Format | Style | Human-readable? | Typical use |
|---|---|---|---|
| JSON | text | Yes | web APIs, configs, microservices |
| XML + | text (tags) | Yes (verbose) | legacy enterprise, SOAP, configs |
| CSV + | text (rows) | Yes | flat tabular exports, spreadsheets |
| Avro / Parquet / ORC | binary | No | big-data storage, analytics |
| Protobuf + | binary | No | fast service-to-service RPC (gRPC) |
5. JSON syntax rules#
JSON is built from objects, arrays, and a small fixed set of value types, with strict punctuation.
A JSON document is one value. Usually that value is an object: an unordered set of
"name": value pairs wrapped in { }. The pieces:
- Object
{ }, key-value pairs; nesting allowed (a value can itself be an object/array). - Pair, each name is followed by a colon
:and pairs are separated by commas,. - String, text inside double quotes. Keys are always strings.
- Number, integers or decimals, no quotes (e.g.
3925,25.7). - Boolean,
trueorfalse(lowercase). - null, a special value meaning "empty" for any valid type.
- Array
[ ], an ordered list of supported values.
{
"firstName": "John",
"lastName": "Doe",
"rollNumber": 3925,
"bodyMassIndex": 25.7,
"married": false,
"phoneNumbers": [2405820570, 750285042],
"address": { "city": "Atlanta", "zipCode": 50005 },
"middleName": null
}[1,2,] ✗), comments (// ... ✗),
single quotes ('x' ✗), and unquoted keys ({x:1} ✗). It also
can't natively represent dates, NaN/Infinity, or binary, those get encoded as
strings/numbers by convention.0750285042 is actually invalid JSON for a number; that's why the example above drops it.
Phone numbers are better stored as strings anyway (you never do math on them, and leading zeros
matter).6. POJO: the Java side of the bridge#
A POJO is a plain Java class of simple fields that Jackson maps to and from JSON.
+ POJO = Plain Old Java Object, an ordinary class with fields (and usually getters/setters), not tied to any framework or special base class. Java holds information as objects, so to (de)serialize I need a class whose shape mirrors the JSON.
public class Member { public String name; public Integer age; public String secretIdentity; }
Given JSON {"name":"JJonahJameson","age":60,"secretIdentity":"OmniMan"}, Jackson lines up
each JSON key with the same-named Java field.
7. Jackson & ObjectMapper#
Jackson is the Java library for JSON; its ObjectMapper is the one object that
does both serialization and deserialization.
Jackson is the de-facto JSON library in Java. The workhorse is ObjectMapper. Two
directions, two methods:
| Goal | Call | Result |
|---|---|---|
| Serialize to file | om.writeValue(outputFile, m) | writes JSON to that file |
| Serialize to String + | om.writeValueAsString(m) | returns JSON as a String |
| Deserialize | om.readValue(input, Member.class) | returns a Member |
+ Correction worth noting: writeValue(File, obj) returns
void (it writes to the file). If you want the JSON as a return value, use
writeValueAsString(obj). The original "String s = om.writeValue(...)" mixes
the two, the file overload doesn't return the string.
How does Jackson know which field maps to which key? By name matching: JSON key
"age" → field/property named age. + Under the hood
Jackson matches against properties, which it discovers from public fields or from getter/setter
method names (getAge() → property age).
ObjectMapper and reuse it, it's thread-safe after configuration and somewhat expensive to
construct. Don't make a new one per call in hot paths.→ See code: ObjectMapper round-trip
8. Activity 1, parsing stock trades & edge cases#
A real array of trade objects maps to a List<Trade>, and poking at the
POJO reveals exactly how Jackson handles mismatches.
Input is an array, so deserialize into a list, not a single object. The questions this activity provokes (and their answers):
| You change… | What Jackson does |
|---|---|
Field symbol → symbl | JSON key "symbol" no longer matches → on deserialize the field stays
null/default; on serialize the key becomes "symbl". |
Field quantity type int → String | Jackson coerces compatible values (number 100 → String "100"
works). + Going the other way, a non-numeric string into an
int throws. |
| Duplicate keys in JSON | + By default the last occurrence wins; no error is raised (can be made strict via a parser feature). |
→ See code: Trade list parsing + edge experiments
9. Activity 2, @JsonProperty when keys don't match
#
When the JSON key and your Java field name differ, @JsonProperty declares the
exact key to bind to, no need to rename your fields.
Sometimes the incoming JSON uses keys you can't or don't want to mirror in Java (e.g.
"1.symbol", or snake_case keys vs camelCase fields). Renaming Java fields to match ugly
keys is bad, so instead annotate:
public class Trade { @JsonProperty("1.symbol") public String symbol; @JsonProperty("2.quantity") public int quantity; @JsonProperty("3.purchaseDate") public String purchaseDate; }
Now "1.symbol" binds to symbol both ways: on deserialize the key fills the
field; on serialize the field is written back out as "1.symbol". + The annotation is bidirectional, it controls both reading and writing.
om.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE). Use
@JsonProperty for one-off oddball keys.→ See code: @JsonProperty mapping
10. Activity 2.1, @JsonIgnoreProperties for unknown
fields#
When JSON contains fields your POJO doesn't have, Jackson throws by default;
@JsonIgnoreProperties(ignoreUnknown=true) tells it to skip them quietly.
If the JSON gains an extra key (say "4.weather":"Rainy") that has no matching field, default
Jackson raises UnrecognizedPropertyException and deserialization fails. You rarely want a
new upstream field to crash your parser, so:
@JsonIgnoreProperties(ignoreUnknown = true) public class Trade { @JsonProperty("1.symbol") public String symbol; @JsonProperty("2.quantity") public int quantity; @JsonProperty("3.purchaseDate") public String purchaseDate; }
The annotation goes on the class. Now unknown keys are ignored and parsing succeeds.
| Variant | Effect |
|---|---|
@JsonIgnoreProperties(ignoreUnknown=true) | ignore any unknown JSON key |
@JsonIgnoreProperties({"weather","note"}) + | ignore those named keys specifically |
@JsonIgnore on a field + | never read/write that one field (e.g. a password) |
om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) applies the same
leniency mapper-wide.→ See code: @JsonIgnoreProperties
Connections between these ideas#
- Persistence → Serialization: wanting data to survive forces you to serialize it.
- Cross-language → JSON: needing many languages to read it forces a neutral text format.
- JSON → Jackson: in Java, JSON ⇄ object conversion is done by Jackson's
ObjectMapper. - POJO names → annotations: when POJO field names can't match JSON keys, annotations bridge the
gap (
@JsonProperty) and handle surprises (@JsonIgnoreProperties).
III. Code Samples (runnable)#
Real, runnable Java + Jackson. Each block has a filename, a run command, and expected output. + Setup uses the Jackson Databind dependency; see the Lab for the one-time setup.
A. ObjectMapper round-trip (serialize + deserialize) #
public class Member { // public fields so Jackson can see them with no getters/setters public String name; public Integer age; public String secretIdentity; // Jackson needs a no-arg constructor to build the object on read public Member() {} public Member(String name, Integer age, String id) { this.name = name; this.age = age; this.secretIdentity = id; } }
import com.fasterxml.jackson.databind.ObjectMapper; public class Main { public static void main(String[] args) throws Exception { ObjectMapper om = new ObjectMapper(); // --- SERIALIZE: object -> JSON string --- Member m = new Member("JJonahJameson", 60, "OmniMan"); String json = om.writeValueAsString(m); // returns the JSON text System.out.println(json); // --- DESERIALIZE: JSON string -> object --- String input = "{\"name\":\"Peter\",\"age\":29,\"secretIdentity\":\"Spidey\"}"; Member back = om.readValue(input, Member.class); System.out.println(back.name + " / " + back.age); } }
readValue Jackson first creates an empty
Member(), then sets each field, so without it you get
InvalidDefinitionException. Serialization doesn't need it.B. Activity 1, parse a list of trades + edge experiments#
public class Trade { public String symbol; public int quantity; public String purchaseDate; @Override public String toString() { return symbol + " x" + quantity + " @" + purchaseDate; } }
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.core.type.TypeReference; import java.util.List; public class ParseTrades { public static void main(String[] a) throws Exception { String jsonArray = "[{\"symbol\":\"AAPL\",\"quantity\":100,\"purchaseDate\":\"2019-01-02\"}," + " {\"symbol\":\"MSFT\",\"quantity\":10,\"purchaseDate\":\"2019-01-02\"}]"; ObjectMapper om = new ObjectMapper(); // TypeReference preserves the generic type List<Trade> at runtime List<Trade> trades = om.readValue(jsonArray, new TypeReference<List<Trade>>() {}); trades.forEach(System.out::println); } }
List.class alone loses the <Trade> part and
you'd get a List<LinkedHashMap>. TypeReference captures the full type so
Jackson builds real Trade objects. (Alternative:
om.readValue(json, Trade[].class) for an array.)Edge experiment outputs (predict, then expand) results
| Experiment | Result |
|---|---|
Rename field symbol→symbl, keep JSON key "symbol"
| symbl stays null; no error. toString() prints
null x100 ... |
Change quantity to String | Number 100 coerced to "100", works fine. |
JSON value "quantity":"abc" into int | Throws InvalidFormatException (can't make int from "abc"). + |
Duplicate key "quantity":100,"quantity":7 | Last wins → quantity == 7; no error by default. +
|
C. Activity 2, @JsonProperty
for mismatched keys#
import com.fasterxml.jackson.annotation.JsonProperty; public class Trade { @JsonProperty("1.symbol") public String symbol; @JsonProperty("2.quantity") public int quantity; @JsonProperty("3.purchaseDate") public String purchaseDate; @Override public String toString() { return symbol + " x" + quantity; } }
String json = "{\"1.symbol\":\"AAPL\",\"2.quantity\":100,\"3.purchaseDate\":\"2019-01-02\"}"; ObjectMapper om = new ObjectMapper(); Trade t = om.readValue(json, Trade.class); // "1.symbol" -> symbol System.out.println(t); String out = om.writeValueAsString(t); // writes "1.symbol" back System.out.println(out);
D. Activity 2.1,
@JsonIgnoreProperties for unknown fields#
import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; @JsonIgnoreProperties(ignoreUnknown = true) // skip keys we don't model public class Trade { @JsonProperty("1.symbol") public String symbol; @JsonProperty("2.quantity") public int quantity; @JsonProperty("3.purchaseDate") public String purchaseDate; }
// note the extra "4.weather" key that Trade has no field for String json = "{\"1.symbol\":\"AAPL\",\"2.quantity\":100," + "\"3.purchaseDate\":\"2019-01-02\",\"4.weather\":\"Rainy\"}"; ObjectMapper om = new ObjectMapper(); Trade t = om.readValue(json, Trade.class); // weather ignored, no crash System.out.println(t.symbol + " " + t.quantity);
@JsonIgnoreProperties and the same input throws:
UnrecognizedPropertyException: Unrecognized field "4.weather" — Jackson refuses unknown
keys by default.
IV. Hands-On Lab (practice)#
Six graded exercises. Predict the output before you run, then verify. Solutions are collapsed.
One-time Setup
Goal: a folder that compiles Java with Jackson on the classpath.
plugins { id 'application' }
repositories { mavenCentral() }
dependencies {
// brings databind + core + annotations transitively
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.1'
}
application { mainClass = 'Main' }javac -cp "libs/*" *.java && java -cp ".:libs/*" Main (use ; instead
of : on Windows).
1. Serialize a single object WARM-UP#
Goal: turn one Member into a JSON string.
- Create
Memberwith fieldsname,age,secretIdentity+ no-arg constructor. - In
Main, build aMemberand callwriteValueAsString. - Print the result.
Solution java
ObjectMapper om = new ObjectMapper(); Member m = new Member("Ada", 36, "Countess"); System.out.println(om.writeValueAsString(m));
+ No spaces by default (compact). Want pretty output?
om.writerWithDefaultPrettyPrinter().writeValueAsString(m).
2. Deserialize back into an object WARM-UP#
Goal: rebuild a Member from a JSON string and read a field.
- Take the JSON from Ex 1 as a string literal (escape the quotes).
- Call
readValue(json, Member.class). - Print
member.nameandmember.age.
Member?
Solution java
String json = "{\"name\":\"Ada\",\"age\":36,\"secretIdentity\":\"Countess\"}"; Member m = new ObjectMapper().readValue(json, Member.class); System.out.println(m.name + " / " + m.age);
+ No no-arg constructor →
InvalidDefinitionException: Cannot construct instance ... no Creators.
3. Parse a JSON array into a List CORE#
Goal: deserialize the two-trade array into List<Trade> and sum the quantities.
- Define
Trade(symbol, quantity, purchaseDate). - Read with
new TypeReference<List<Trade>>(){}. - Loop and add up
quantity.
List.class instead of
TypeReference, what type are the list elements?Solution java
String arr = "[{\"symbol\":\"AAPL\",\"quantity\":100,\"purchaseDate\":\"2019-01-02\"}," + " {\"symbol\":\"MSFT\",\"quantity\":10,\"purchaseDate\":\"2019-01-02\"}]"; List<Trade> ts = new ObjectMapper() .readValue(arr, new TypeReference<List<Trade>>(){}); int total = ts.stream().mapToInt(t -> t.quantity).sum(); System.out.println("total = " + total);
+ With List.class the elements are
LinkedHashMap, not Trade — accessing t.quantity wouldn't
even compile.
4. Fix a name mismatch with @JsonProperty CORE#
Goal: bind JSON keys "1.symbol", "2.quantity",
"3.purchaseDate" to clean fields.
- Start from a
Tradewhose fields aresymbol/quantity/purchaseDate. - Run against the dotted-key JSON, observe nulls/zeros.
- Add
@JsonProperty("…")to each field, re-run.
quantity (an
int) when its key doesn't match?Solution java
public class Trade { @JsonProperty("1.symbol") public String symbol; @JsonProperty("2.quantity") public int quantity; @JsonProperty("3.purchaseDate") public String purchaseDate; }
+ Before the fix: symbol == null,
quantity == 0 (int default). A primitive can't be null, so the tell is
0, not null.
5. Survive an unknown field CORE #
Goal: deserialize JSON that has an extra "4.weather" key without crashing.
- Use the annotated
Tradefrom Ex 4. - Feed it JSON containing
"4.weather":"Rainy", observe the exception. - Add
@JsonIgnoreProperties(ignoreUnknown=true)to the class, re-run.
Solution java
@JsonIgnoreProperties(ignoreUnknown = true) public class Trade { /* annotated fields as in Ex 4 */ }
+ Before:
UnrecognizedPropertyException: Unrecognized field "4.weather". Mapper-wide
alternative:
om.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).
6. Hide a field + handle a duplicate key CHALLENGE#
Goal: never serialize a sensitive field, and observe duplicate-key behavior.
- Add a
passwordfield to a class and mark it@JsonIgnore; serialize and confirm it's absent. - Deserialize JSON with a repeated key (e.g.
"quantity":100,"quantity":7) and print the value. - Explain which one won and why no exception fired.
@JsonIgnore block both reading and writing?Solution java
import com.fasterxml.jackson.annotation.JsonIgnore; public class User { public String username; @JsonIgnore public String password; // skipped both ways }
ObjectMapper om = new ObjectMapper(); User u = new User(); u.username = "ada"; u.password = "secret"; System.out.println(om.writeValueAsString(u)); // password omitted String dup = "{\"quantity\":100,\"quantity\":7}"; Trade t = om.readValue(dup, Trade.class); // Trade w/o @JsonProperty System.out.println("quantity = " + t.quantity);
+ Last duplicate wins because Jackson processes keys in stream order,
overwriting earlier values; standard JSON parsers don't error on duplicates by default.
@JsonIgnore removes the property from both serialization and deserialization. To
reject duplicates, enable JsonParser.Feature.STRICT_DUPLICATE_DETECTION.
Self-check rubric, how to know you've got it#
- I can state serialization vs deserialization in one sentence each, with direction.
- I can write valid JSON from memory and name 3 things JSON forbids.
- I can serialize and deserialize a POJO with
ObjectMapperwithout looking it up. - Given a null field after deserialization, my first instinct is "key/field name mismatch."
- I know which annotation fixes mismatched keys (
@JsonProperty) vs unknown keys (@JsonIgnoreProperties) vs hiding a field (@JsonIgnore). - I can explain why
List<Trade>needsTypeReference. - I can predict duplicate-key and type-coercion behavior.
★ Key Takeaways#
- Memory is temporary. Collections die with the program; persistence = writing to file/DB/network.
- Cross-language sharing needs a neutral text format, which on the internet is overwhelmingly JSON.
- Serialization = object → format; deserialization = format → object. Same idea as encode/decode.
- JSON = objects
{}, arrays[], strings, numbers, booleans, null. Strict punctuation, no trailing commas/comments/single quotes. - Jackson's
ObjectMapperdoes both directions:writeValue(AsString)out,readValuein. Mapping is by field name. - POJOs are the schema. Need a no-arg constructor for deserialization;
TypeReferencefor generic collections. - Annotations bridge the gaps:
@JsonProperty(rename),@JsonIgnoreProperties(ignoreUnknown=true)(tolerate extras),@JsonIgnore(hide). - Edge behavior to memorize: mismatched key → null/default (no error); duplicate key → last wins; unknown field → throws unless ignored; type mismatch → coerced if possible else throws.