Skip to content

Add ShapeTranscoder for Shape->Shape#1299

Open
mtdowling wants to merge 2 commits into
mainfrom
ShapeTranscoder
Open

Add ShapeTranscoder for Shape->Shape#1299
mtdowling wants to merge 2 commits into
mainfrom
ShapeTranscoder

Conversation

@mtdowling

@mtdowling mtdowling commented Jul 23, 2026

Copy link
Copy Markdown
Member

What behavior changes?

Adds ShapeTranscoder for converting compatible generated shapes directly through their serializer/deserializer implementations, without needing an intermediate Document.

SourceRequest source = SourceRequest.builder().name("example").count(42).build();

// Convenience API.
TargetRequest target = ShapeTranscoder.convert(source, TargetRequest.builder());

// Strict conversion skips client error correction and rejects lossy conversions.
TargetRequest strictTarget = ShapeTranscoder.convertStrict(source, TargetRequest.builder());

// Reuse a transcoder when performing repeated conversions on one thread.
var transcoder = new ShapeTranscoder();
TargetRequest reused = transcoder.transcode(source, TargetRequest.builder());

Members are matched by Smithy member name. Source-only members not in the target shape are dropped, while missing required target members are handled by normal builder error correction in convert() and fail during convertStrict().

We permit lossless numeric conversions, even in strict mode, including long to BigInteger, but we reject overflow, fractional truncation, and floating-point rounding.

The transcoder streams values synchronously using reusable cursors. It buffers one value per nesting level rather than materializing the entire shape, and never calls getMemberValue().

Why is this change needed?

Services often don't share shapes across service boundaries, despite the shapes have essentially the same shape. For example, there are various DynamoDB-like services that all use AttributeValue, but each generated shape are incompatible types. Converting between generated types was possible before, but previously required an intermediate "DOM" representation through documents:

TargetRequest target = Document.of(source).asShape(TargetRequest.builder());

That approach allocates a complete document tree and traverses the shape twice. This feature provides a lower-allocation, lower-latency path for model adaptation, version migration, generated-type interoperability, and copying compatible request or response shapes.

Reusable transcoders retain their cursor pool between calls. Structure member mappings are cached through a bounded schema extension to avoid repeatedly rebuilding mappings while keeping extension computation thread-safe and behaviorally immutable.

How was this validated?

Added tests covering:

  • Distinct generated structure graphs without getMemberValue().
  • Scalars, structures, lists, maps, nulls, documents, blobs, timestamps, and streams.
  • Source-only member dropping and missing required target members.
  • Default error correction versus strict direct building.
  • Lossless and lossy cross-kind numeric conversions.
  • Arbitrary-precision numeric conversion.
  • Reuse after successful and failed conversions.
  • Concurrent schema-extension creation and bounded cache eviction.

Added JMH comparisons against the Document intermediary:

 Shape                        Document intermediary         ShapeTranscoder
 ━━━━━━━━━━━━━━━━━━━━━━━━━━━  ━━━━━━━━━━━━━━━━━━━━━━━  ━━━━━━━━━━━━━━━━━━━━━━
 Shallow                        203.18 ns, 784 B/op       66.01 ns, 64 B/op
 ───────────────────────────  ───────────────────────  ──────────────────────
 Deep complex                 24.00 us, 98,192 B/op    8.36 us, 12,768 B/op
 ───────────────────────────  ───────────────────────  ──────────────────────
 Nested DDB AttributeValue    16.11 us, 83,680 B/op     7.20 us, 9,376 B/op

Validation commands:

./gradlew spotlessApply
./gradlew spotlessCheck
./gradlew :core:check

What should reviewers focus on?

  • ShapeTranscoder.java: cursor state machine, synchronous serializer-to-deserializer forwarding, strict numeric compatibility, and cleanup after failures.
  • ShapeTranscoderSchemaExtensions.java: bounded member-mapping cache, benign extension creation races, and atomic cache updates.
  • Schema.java and SchemaExtensionProvider.java: schema-extension lifecycle and publication requirements.

Additional Links

Related issues, design docs, or prior art.


By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant