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)

This commit is contained in:
2026-10-08 17:00:53 +00:00
parent 0157132e9d
commit 84070a8dfb
6 changed files with 130 additions and 6 deletions
+10 -5
View File
@@ -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
@@ -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')
+2
View File
@@ -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"}
+5 -1
View File
@@ -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
@@ -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<T> written directly, without the StdSerializer detour.
* This is the other half: ValueDeserializer<T> 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<UserId> {
@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<UserId> {
@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(""));
}
}
}
@@ -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));
}
}