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 main Jun 16, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant