Skip to content

feat: own a SerdeException type and reject lenient scalar coercion - #93

Merged
OmarAlJarrah merged 3 commits into
mainfrom
feat/serde-error-and-coercion
Jun 16, 2026
Merged

feat: own a SerdeException type and reject lenient scalar coercion#93
OmarAlJarrah merged 3 commits into
mainfrom
feat/serde-error-and-coercion

Conversation

@OmarAlJarrah

Copy link
Copy Markdown
Member

Summary

Two serde-robustness changes.

SDK-owned SerdeException (#22)

Introduces SerdeException (with SerializationException / DeserializationException subtypes) in the serde SPI. The Jackson implementation now catches Jackson's JsonProcessingException hierarchy at the SPI boundary and rethrows as these SDK types with the original cause chained, so Jackson exception types no longer leak across the Serde abstraction. A genuine stream IOException (not a Jackson type) still propagates unchanged, preserving the caller-owned-stream contract.

Reject lenient scalar coercion in the default mapper (#24)

The SDK's default ObjectMapper previously inherited Jackson's lenient scalar coercions, which silently turn malformed payloads into the wrong type. The default mapper now rejects the lenient cases — e.g. "5"→int, "1.5"→double, "true"→boolean, and number/boolean→string — while leaving numeric widening and genuinely-typed payloads working. Caller-supplied mappers are untouched. (Configured at build time via JsonMapper.builder() to avoid the deprecated runtime feature setters.)

New tests cover the exception wrapping and each disabled coercion. SerdeException is added to the API snapshot.

Closes#22
Closes#24

…cion
Two hardening changes to the serde layer.
1. Stable SPI failure type. The `serde` SPI declared no exception type, so the
Jackson adapter let `com.fasterxml.jackson.*` exceptions escape across the
zero-dependency `Serde` boundary. Callers coding against the SPI could not
catch a stable type, and the abstraction leaked its backing library. Add an
open `SerdeException : RuntimeException` to `sdk-core`'s serde package, plus
`SerializationException` / `DeserializationException` for the write and read
directions. The Jackson adapter now catches `JsonProcessingException` (the
root of Jackson's parse/mapping failure hierarchy) at every SPI method and
rethrows as the matching SDK type with the original chained as the cause. A
genuine stream `IOException` still propagates unchanged, and the buffer
overload's bounds checks remain `IndexOutOfBoundsException`.
2. Strict scalar coercion on the default mapper. Jackson's defaults silently
reshape mismatched scalars: a wire string "5" coerced into a numeric field,
numbers into strings, booleans across types. That masks malformed payloads.
The SDK's default `ObjectMapper` now disables
`MapperFeature.ALLOW_COERCION_OF_SCALARS` and sets per-type coercion to
`Fail` for the cross-shape pairs (string -> int/float/boolean, boolean <->
int, and int/float/boolean -> string), built through
`JsonMapper.builder().withCoercionConfig(...)`. Numeric widening
(int -> floating-point) and correctly typed payloads are unaffected, and the
auto-detect features Kotlin data-class binding relies on are left untouched.
This applies only to the SDK default mapper, never to a caller-supplied one.
This is a pre-1.0 behaviour change: payloads whose JSON shape does not match the
target type now fail instead of binding to a quietly wrong value.
…Exception contract
The strict-coercion config rejected string/boolean into integer fields but had
no rule for a floating-point JSON value into an integer field, so {"n":1.5}
silently bound into an Int as 1. That truncation turns a contract-violating
payload into a quietly wrong value, the exact class of bug the lockdown exists
to prevent. Add CoercionInputShape.Float to the LogicalType.Integer fail list
and pin the rejection with a test.
Also document the failure contract on the SPI itself: the core Serializer /
Deserializer interfaces now carry @throws SerializationException /
DeserializationException notes, and the internal Jackson overrides mirror them,
so a reader of the SPI sees the expected stable failure type rather than
learning it only from the Jackson adapter.
…default mapper
Extend the default ObjectMapper's coercion lockdown to two cases the
initial rules left open:
- boolean -> floating-point is now rejected, matching the existing
boolean<->integer rejections, so `true` cannot become `1.0`.
- an empty string into an integer/floating-point/boolean field is now
rejected. Jackson otherwise coerces `""` to a null/zero scalar, which
silently masks a malformed payload. An empty string into a textual
field is still accepted — `""` is a legitimate string value.
Tests cover each new rejection plus the empty-string-binds-to-string case.
@OmarAlJarrah
OmarAlJarrah merged commit 79ad548 into mainJun 16, 2026
1 check passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant

@OmarAlJarrah