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).
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 [+]
| 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
| 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 gaps
One numbered concept each: claim → plain-English build-up → why it matters → traps. + marks my additions and corrections.
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.
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. +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
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) |
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:
{ }, key-value pairs; nesting allowed (a value can itself be an
object/array).: and pairs are separated by commas
,.3925, 25.7).true or false (lowercase).[ ], 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).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.
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
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
@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
@JsonIgnoreProperties for unknown
fieldsWhen 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
ObjectMapper.@JsonProperty) and handle surprises (@JsonIgnoreProperties).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.
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.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.)| 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. +
|
@JsonProperty
for mismatched keysimport 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);
@JsonIgnoreProperties for unknown fieldsimport 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.
Six graded exercises. Predict the output before you run, then verify. Solutions are collapsed.
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).
Goal: turn one Member into a JSON string.
Member with fields name, age,
secretIdentity + no-arg constructor.Main, build a Member and call writeValueAsString.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).
Goal: rebuild a Member from a JSON string and read a field.
readValue(json, Member.class).member.name and member.age.Member?
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.
Goal: deserialize the two-trade array into List<Trade> and sum the quantities.
Trade (symbol, quantity, purchaseDate).new TypeReference<List<Trade>>(){}.quantity.List.class instead of
TypeReference, what type are the list elements?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.
Goal: bind JSON keys "1.symbol", "2.quantity",
"3.purchaseDate" to clean fields.
Trade whose fields are
symbol/quantity/purchaseDate.@JsonProperty("…") to each field, re-run.quantity (an
int) when its key doesn't match?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.
Goal: deserialize JSON that has an extra "4.weather" key without crashing.
Trade from Ex 4."4.weather":"Rainy", observe the exception.@JsonIgnoreProperties(ignoreUnknown=true) to the class, re-run.@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).
Goal: never serialize a sensitive field, and observe duplicate-key behavior.
password field to a class and mark it @JsonIgnore; serialize and
confirm it's absent."quantity":100,"quantity":7) and print the
value.@JsonIgnore block both reading and writing?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.
ObjectMapper without looking it up.@JsonProperty) vs unknown keys
(@JsonIgnoreProperties) vs hiding a field (@JsonIgnore).List<Trade> needs TypeReference.{}, arrays [], strings, numbers, booleans, null.
Strict punctuation, no trailing commas/comments/single quotes.ObjectMapper does both directions: writeValue(AsString)
out, readValue in. Mapping is by field name.TypeReference for generic collections.@JsonProperty (rename),
@JsonIgnoreProperties(ignoreUnknown=true) (tolerate extras), @JsonIgnore
(hide).