From 84070a8dfba42470668b6cc52d681d49ccdcd330 Mon Sep 17 00:00:00 2001 From: Ankur Date: Thu, 8 Oct 2026 17:00:53 +0000 Subject: [PATCH] part4custom: add E06 (ValueDeserializer direct) and E07 (mix-in on a record), fix run-all.sh exit-code detection through the grep filter (post #50) --- README.md | 15 +++-- docs/output/E06ValueDeserializerDirect.txt | 4 ++ docs/output/E07MixinOnRecord.txt | 2 + run-all.sh | 6 +- .../E06ValueDeserializerDirect.java | 65 +++++++++++++++++++ .../part4custom/E07MixinOnRecord.java | 44 +++++++++++++ 6 files changed, 130 insertions(+), 6 deletions(-) create mode 100644 docs/output/E06ValueDeserializerDirect.txt create mode 100644 docs/output/E07MixinOnRecord.txt create mode 100644 src/main/java/com/ankurm/jackson3/part4custom/E06ValueDeserializerDirect.java create mode 100644 src/main/java/com/ankurm/jackson3/part4custom/E07MixinOnRecord.java diff --git a/README.md b/README.md index a837048..abe57c0 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ nothing is transcribed by hand. ``` Jackson 3.2.3 (tools.jackson.core:jackson-databind) jackson-annotations 2.22 (com.fasterxml.jackson.core — deliberately unchanged) -JDK Temurin 21.0.5 +JDK Temurin 25.0.4.1 (LTS) Maven 3.9.9 ``` @@ -18,7 +18,7 @@ Maven 3.9.9 ```bash git clone https://ankurm.com/git.app/asmhatre/jackson3-by-example.git cd jackson3-by-example -./run-all.sh # compiles, runs all 37 examples, refreshes docs/output/ +./run-all.sh # compiles, runs all 39 examples, refreshes docs/output/ ``` Or run any single example: @@ -36,7 +36,7 @@ mvn -q exec:java -Dexec.mainClass=com.ankurm.jackson3.part5polymorphic.F01Serial | [ObjectMapper Guide](https://ankurm.com/jackson-objectmapper-guide/) | [`part1objectmapper`](src/main/java/com/ankurm/jackson3/part1objectmapper) | [B01](src/main/java/com/ankurm/jackson3/part1objectmapper/B01WriteJson.java) [B02](src/main/java/com/ankurm/jackson3/part1objectmapper/B02ReadJson.java) [B03](src/main/java/com/ankurm/jackson3/part1objectmapper/B03GenericCollections.java) [B04](src/main/java/com/ankurm/jackson3/part1objectmapper/B04PropertyOrdering.java) | [write-up](docs/part1-objectmapper.md) | | [Records, Optionals, Sealed](https://ankurm.com/jackson-java-records-optionals/) | [`part2modernjava`](src/main/java/com/ankurm/jackson3/part2modernjava) | [C01](src/main/java/com/ankurm/jackson3/part2modernjava/C01RecordRoundTrip.java) [C02](src/main/java/com/ankurm/jackson3/part2modernjava/C02OptionalFields.java) [C03](src/main/java/com/ankurm/jackson3/part2modernjava/C03SealedWithSubTypes.java) [C04](src/main/java/com/ankurm/jackson3/part2modernjava/C04SealedAutoDiscovery.java) | [write-up](docs/part2-modern-java.md) | | [Annotations Cheat Sheet](https://ankurm.com/jackson-annotations-guide/) | [`part3annotations`](src/main/java/com/ankurm/jackson3/part3annotations) | [D01](src/main/java/com/ankurm/jackson3/part3annotations/D01RenameAndIgnore.java) [D02](src/main/java/com/ankurm/jackson3/part3annotations/D02InclusionAndFormat.java) [D03](src/main/java/com/ankurm/jackson3/part3annotations/D03AliasAndUnknownFields.java) [D04](src/main/java/com/ankurm/jackson3/part3annotations/D04CreatorsAndUnwrapping.java) | [write-up](docs/part3-annotations.md) | -| [Custom Serialisers & Mix-ins](https://ankurm.com/jackson-custom-serializer-mixin/) | [`part4custom`](src/main/java/com/ankurm/jackson3/part4custom) | [E01](src/main/java/com/ankurm/jackson3/part4custom/E01MoneyValueSerializer.java) [E02](src/main/java/com/ankurm/jackson3/part4custom/E02MoneyValueDeserializer.java) [E03](src/main/java/com/ankurm/jackson3/part4custom/E03SimpleModuleRegistration.java) [E04](src/main/java/com/ankurm/jackson3/part4custom/E04MixinAnnotations.java) [E05](src/main/java/com/ankurm/jackson3/part4custom/E05ValueSerializerDirect.java) | [write-up](docs/part4-custom.md) | +| [Custom Serialisers & Mix-ins](https://ankurm.com/jackson-custom-serializer-mixin/) | [`part4custom`](src/main/java/com/ankurm/jackson3/part4custom) | [E01](src/main/java/com/ankurm/jackson3/part4custom/E01MoneyValueSerializer.java) [E02](src/main/java/com/ankurm/jackson3/part4custom/E02MoneyValueDeserializer.java) [E03](src/main/java/com/ankurm/jackson3/part4custom/E03SimpleModuleRegistration.java) [E04](src/main/java/com/ankurm/jackson3/part4custom/E04MixinAnnotations.java) [E05](src/main/java/com/ankurm/jackson3/part4custom/E05ValueSerializerDirect.java) [E06](src/main/java/com/ankurm/jackson3/part4custom/E06ValueDeserializerDirect.java) [E07](src/main/java/com/ankurm/jackson3/part4custom/E07MixinOnRecord.java) | [write-up](docs/part4-custom.md) (now superseded by the rewritten post itself — see below) | | [Polymorphic Deserialisation](https://ankurm.com/jackson-polymorphic-deserialization/) | [`part5polymorphic`](src/main/java/com/ankurm/jackson3/part5polymorphic) | [F01](src/main/java/com/ankurm/jackson3/part5polymorphic/F01SerialiseMixedList.java) [F02](src/main/java/com/ankurm/jackson3/part5polymorphic/F02DeserialiseMixedList.java) [F03](src/main/java/com/ankurm/jackson3/part5polymorphic/F03IncludeStrategies.java) [F04](src/main/java/com/ankurm/jackson3/part5polymorphic/F04UnknownTypeId.java) | [write-up](docs/part5-polymorphic.md) | | [Streaming API & JsonNode](https://ankurm.com/jackson-streaming-api-jsonnode/) | [`part6streaming`](src/main/java/com/ankurm/jackson3/part6streaming) | [G01](src/main/java/com/ankurm/jackson3/part6streaming/G01StreamingParserFilter.java) [G02](src/main/java/com/ankurm/jackson3/part6streaming/G02StreamingGenerator.java) [G03](src/main/java/com/ankurm/jackson3/part6streaming/G03TreeModelNavigation.java) [G04](src/main/java/com/ankurm/jackson3/part6streaming/G04ThreeApproachesMeasured.java) | [write-up](docs/part6-streaming.md) | | [Security Best Practices](https://ankurm.com/jackson-security-best-practices/) | [`part7security`](src/main/java/com/ankurm/jackson3/part7security) | [H01](src/main/java/com/ankurm/jackson3/part7security/H01SafePolymorphismByAnnotation.java) [H02](src/main/java/com/ankurm/jackson3/part7security/H02DefaultTypingRemoved.java) [H03](src/main/java/com/ankurm/jackson3/part7security/H03PolymorphicTypeValidatorAllowlist.java) [H04](src/main/java/com/ankurm/jackson3/part7security/H04StreamReadConstraints.java) [H05](src/main/java/com/ankurm/jackson3/part7security/H05NeverDeserialiseIntoObject.java) | [write-up](docs/part7-security.md) | @@ -53,13 +53,13 @@ the library disagree. | [docs/part1-objectmapper.md](docs/part1-objectmapper.md) | Reading, writing, `TypeReference`, property ordering | | [docs/part2-modern-java.md](docs/part2-modern-java.md) | Records, `Optional`, sealed types and auto-discovery | | [docs/part3-annotations.md](docs/part3-annotations.md) | The annotation set, and which advice is now obsolete | -| [docs/part4-custom.md](docs/part4-custom.md) | `ValueSerializer`, `ValueDeserializer`, modules, mix-ins | +| [docs/part4-custom.md](docs/part4-custom.md) | `ValueSerializer`, `ValueDeserializer`, modules, mix-ins — superseded by the rewritten post's own in-page depth; kept here for history, not extended further | | [docs/part5-polymorphic.md](docs/part5-polymorphic.md) | `@JsonTypeInfo`, and the dropped-discriminator defect in full | | [docs/part6-streaming.md](docs/part6-streaming.md) | Streaming, the tree model, and the three approaches measured | | [docs/part7-security.md](docs/part7-security.md) | Safe polymorphism, allowlists, resource limits | | [docs/beyond.md](docs/beyond.md) | Unchecked exceptions, flipped defaults, pool tuning | -Raw stdout for all 37 programs is in [`docs/output/`](docs/output), regenerated by +Raw stdout for all 39 programs is in [`docs/output/`](docs/output), regenerated by [`run-all.sh`](run-all.sh). ## Corrections to the posts @@ -90,6 +90,11 @@ Two smaller ones worth knowing, neither strictly an error in the posts: - `{"amount":20.00}` deserialises to `BigDecimal` **20.0**, not 20.00 — the scale is lost unless `USE_BIG_DECIMAL_FOR_FLOATS` is enabled. See [E03](src/main/java/com/ankurm/jackson3/part4custom/E03SimpleModuleRegistration.java). +- `addMixIn()` works on a `record`, not just a field-and-getter class — but the mix-in has + to declare methods named like the record's own accessors (`postalCode()`), not + getter-style names (`getPostalCode()`), because that is what a record's introspected + properties are actually called. See + [E07](src/main/java/com/ankurm/jackson3/part4custom/E07MixinOnRecord.java). ## Measured, not asserted diff --git a/docs/output/E06ValueDeserializerDirect.txt b/docs/output/E06ValueDeserializerDirect.txt new file mode 100644 index 0000000..c31a1c4 --- /dev/null +++ b/docs/output/E06ValueDeserializerDirect.txt @@ -0,0 +1,4 @@ +serialised : {"assignee":"u-42","title":"Fix build"} +round-trip : Ticket[assignee=UserId[value=u-42], title=Fix build] +equal : true +no module : MismatchedInputException - Cannot construct instance of `com.ankurm.jackson3.part4custom.E06ValueDeserializerDirect$UserId` (although at least one Creator exists): no String-argument constructor/factory method to deserialize from String value ('u-42') diff --git a/docs/output/E07MixinOnRecord.txt b/docs/output/E07MixinOnRecord.txt new file mode 100644 index 0000000..fdf38e5 --- /dev/null +++ b/docs/output/E07MixinOnRecord.txt @@ -0,0 +1,2 @@ +with mixin : {"street":"123 Main St","city":"Springfield","zip":"12345"} +without mixin: {"street":"123 Main St","city":"Springfield","postalCode":"12345","internalTrackingCode":"INTERNAL-X99"} diff --git a/run-all.sh b/run-all.sh index 9a76ab8..c37b532 100755 --- a/run-all.sh +++ b/run-all.sh @@ -15,7 +15,11 @@ for f in $(find src/main/java -name '*.java' | sort); do grep -q 'public static void main' "$f" || continue SHORT="${CLASS##*.}" printf '%-52s' "$SHORT" - if java -cp "$CP" "$CLASS" > "docs/output/$SHORT.txt" 2>&1; then + # grep -v strips the sandbox's injected "Picked up JAVA_TOOL_OPTIONS" proxy-config + # notice from stderr - it is an artifact of this environment, not program output. + # PIPESTATUS[0] (not grep's own exit code) is what tells us whether java itself failed. + java -cp "$CP" "$CLASS" 2>&1 | grep -v '^Picked up JAVA_TOOL_OPTIONS' > "docs/output/$SHORT.txt" + if [ "${PIPESTATUS[0]}" -eq 0 ]; then echo "ok" else echo "FAILED"; FAILED=1 diff --git a/src/main/java/com/ankurm/jackson3/part4custom/E06ValueDeserializerDirect.java b/src/main/java/com/ankurm/jackson3/part4custom/E06ValueDeserializerDirect.java new file mode 100644 index 0000000..070d281 --- /dev/null +++ b/src/main/java/com/ankurm/jackson3/part4custom/E06ValueDeserializerDirect.java @@ -0,0 +1,65 @@ +package com.ankurm.jackson3.part4custom; + +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.ValueDeserializer; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.module.SimpleModule; + +/** + * BEYOND THE POST — the deserialising half of E05ValueSerializerDirect. + * + * E05 showed ValueSerializer written directly, without the StdSerializer detour. + * This is the other half: ValueDeserializer written directly, without StdDeserializer. + * Together they round-trip UserId as a bare JSON string ("u-42") instead of the + * structural {"value":"u-42"} a record would otherwise produce. + */ +public class E06ValueDeserializerDirect { + + public record UserId(String value) { } + + static class UserIdSerializer extends tools.jackson.databind.ValueSerializer { + @Override + public void serialize(UserId id, tools.jackson.core.JsonGenerator gen, + tools.jackson.databind.SerializationContext ctxt) { + gen.writeString(id.value()); + } + } + + /** Reads the bare JSON string straight back into the wrapper type. */ + static class UserIdDeserializer extends ValueDeserializer { + @Override + public UserId deserialize(JsonParser p, DeserializationContext ctxt) { + return new UserId(p.getString()); + } + } + + public record Ticket(UserId assignee, String title) { } + + public static void main(String[] args) { + SimpleModule module = new SimpleModule("UserIdModule"); + module.addSerializer(UserId.class, new UserIdSerializer()); + module.addDeserializer(UserId.class, new UserIdDeserializer()); + + JsonMapper mapper = JsonMapper.builder().addModule(module).build(); + + Ticket original = new Ticket(new UserId("u-42"), "Fix build"); + String json = mapper.writeValueAsString(original); + System.out.println("serialised : " + json); + + Ticket restored = mapper.readValue(json, Ticket.class); + System.out.println("round-trip : " + restored); + System.out.println("equal : " + original.equals(restored)); + + // Without the module, Jackson can't turn a bare JSON string "u-42" into a + // one-component record - it expects {"value":"u-42"} and this throws. + JsonMapper plain = JsonMapper.builder().build(); + try { + plain.readValue("{\"assignee\":\"u-42\",\"title\":\"Fix build\"}", Ticket.class); + System.out.println("no module : (unexpectedly succeeded)"); + } catch (Exception e) { + System.out.println("no module : " + e.getClass().getSimpleName() + + " - " + e.getMessage().lines().findFirst().orElse("")); + } + } +} diff --git a/src/main/java/com/ankurm/jackson3/part4custom/E07MixinOnRecord.java b/src/main/java/com/ankurm/jackson3/part4custom/E07MixinOnRecord.java new file mode 100644 index 0000000..48f5c20 --- /dev/null +++ b/src/main/java/com/ankurm/jackson3/part4custom/E07MixinOnRecord.java @@ -0,0 +1,44 @@ +package com.ankurm.jackson3.part4custom; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; +import tools.jackson.databind.json.JsonMapper; + +/** + * BEYOND THE POST — does addMixIn() still work when the third-party type is a + * record, not a field-and-getter class like E04's Address? + * + * A record's "properties" are its canonical accessor methods (postalCode(), not + * getPostalCode()), so a mixin has to declare methods with those exact names for + * Jackson's annotation-introspection merge to find them. Run this before assuming + * either way - the answer below was obtained by actually running it, not inferred + * from how E04's field-based mixin behaves. + */ +public class E07MixinOnRecord { + + /** Stand-in for a third-party record type whose source you cannot modify. */ + public record AddressRecord(String street, String city, String postalCode, + String internalTrackingCode) { } + + /** Mix-in for AddressRecord: method signatures, not fields, to match a record's accessors. */ + public interface AddressRecordMixin { + @JsonIgnore + String internalTrackingCode(); + + @JsonProperty("zip") + String postalCode(); + } + + public static void main(String[] args) { + AddressRecord address = new AddressRecord( + "123 Main St", "Springfield", "12345", "INTERNAL-X99"); + + JsonMapper withMixin = JsonMapper.builder() + .addMixIn(AddressRecord.class, AddressRecordMixin.class) + .build(); + System.out.println("with mixin : " + withMixin.writeValueAsString(address)); + + JsonMapper withoutMixin = JsonMapper.builder().build(); + System.out.println("without mixin: " + withoutMixin.writeValueAsString(address)); + } +}