Sitelet https://github.com/aws/aws-sdk-cpp/pull/3947
Skip to content

Schema Serde Document type & Codec Settings - #3947

Draft
sbaluja wants to merge 10 commits into
mainfrom
document-type
Draft

sbaluja wants to merge 10 commits into
mainfrom
document-type

Conversation

@sbaluja

@sbaluja sbaluja commented Sep 30, 2026

Copy link
Copy Markdown
Collaborator

Issue #, if available:

Description of changes:

  • Added smithy::schema::Document (open-content value). JSON builds and serializes it, CBOR/XML/Query reject or ignore it per protocol.
  • JSON document coercion: base64 string to blob, string/number to timestamp, plus numeric overflow/UB guards (ReadLong clamps, AsFloat/AsLong range-guarded).
  • Added CodecSettings: per-protocol default @timestampFormat. Format is required (no default construction, explicit ctor).
  • Threaded CodecSettings through the JSON, XML, and Query serializers and deserializers. Removed the scattered hardcoded date-time/epoch literals so there's one source per protocol.
  • Gated document string/number to timestamp coercion by the configured format (SEP): string only under date-time/http-date, number only under epoch.
  • CBOR ignores @timestampFormat (epoch only), locked in with a test.
  • JSON floats serialize at float precision, doubles keep a .0 marker so the type round-trips.

Check all that applies:

  • Did a review by yourself.
  • Added proper tests to cover this PR. (If tests are not applicable, explain.)
  • Checked if this PR is a breaking (APIs have been changed) change.
  • Checked if this PR will not introduce cross-platform inconsistent behavior.
  • Checked if this PR would require a ReadMe/Wiki update.

Check which platforms you have built SDK on to verify the correctness of this PR.

  • Linux
  • Windows
  • Android
  • MacOS
  • IOS
  • Other Platforms

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

Adds an open-content document value type to the schema-serde runtime and
wires it through the JSON/CBOR/XML/Query serializers and deserializers.

- smithy::schema::Document: a copyable value handle over a polymorphic,
  AWS_CORE_LOCAL DocumentImpl. The agnostic base serves serialize-built (and
  future CBOR) documents; JsonDocumentImpl adds JSON string->blob and
  string/number->timestamp coercion, parameterized by the protocol-default
  timestamp format threaded from JsonCodec.
- Serialization: JSON and CBOR emit documents; REST-XML, AWS/Query, and
  EC2/Query reject with a SerializationException.
- Deserialization: JSON builds the document tree; CBOR and XML reject (CBOR
  ReadDocument consumes the value so sibling members stay aligned).
- JSON numeric safety: ReadLong's double fallback rejects non-finite /
  out-of-range doubles instead of casting them (undefined behavior), while a
  plain-integer overflow still clamps to INT64_MAX/MIN; open-content document
  integer overflow preserves magnitude as a double; Document::AsFloat rejects
  finite doubles beyond float range (out-of-range double->float is undefined
  behavior).
- JSON floats and doubles serialize with shortest-round-trip precision (float
  precision for floats, not the double expansion of their imprecision) and a
  decimal marker, so a document float or double round-trips without losing
  precision or type.

Deferred (documented): map insertion order, int/bigInteger reporting,
unsigned 64-bit, a nesting depth cap of 64, and the tagged-union node layout.
class Schema;

// Per-protocol timestamp default threaded to a codec's serializer and deserializer.
struct CodecSettings {

@pulimsr pulimsr Oct 1, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: could we add useJsonName and useTimestampFormat booleans to CodecSettings, like smithy-java's JsonCodecbuilder. Fine as a follow-up.

}

private:
static constexpr int MAX_DOCUMENT_DEPTH = 64;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAX_DOCUMENT_DEPTH = 64 may be too low: cJSON (Aws::Utils::Document) allows 1000

m_bool = value;
}

void DocumentImpl::SetInteger(int64_t value) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SetInteger always reports ShapeType::Long, so a JSON 0 comes back as Long. I think it should report the first type that holds the value without loss, in this order: Integer, Long, BigInteger, Double, BigDecimal, like smithy-java (parsing, type mapping).

// Per-protocol timestamp default threaded to a codec's serializer and deserializer.
struct CodecSettings {
explicit CodecSettings(TimestampFormatTrait::Format defaultTimestampFormat) : defaultTimestampFormat(defaultTimestampFormat) {}
TimestampFormatTrait::Format defaultTimestampFormat;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit prolly should be m_timestampForamt

Document MakeDocument(std::shared_ptr<const DocumentImpl> impl) { return Document(std::move(impl)); }
} // namespace detail

Document Document::Null() { return detail::MakeDocument(DocumentImpl::MakeNull()); }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

feels like DocumentImpl:: we shoudlnt be accessing this things statically if we have a pointer to implementation in this class. it feels like we are side-caring static functions in polymorphic containter, where we should just be delegating to the underlying implementation.

return HashingUtils::Base64Decode(*encoded);
}

Aws::Crt::Optional<Document> ReadDocument(const Schema&) override { return ReadDocumentValue(0); }

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the other reads all skip the value before returning empty when they fail, like ReadLong and ReadBoolean both call SkipValue first. ReadDocument doesn't, so if ReadDocumentValue fails partway through we're left sitting in the middle of the value and the struct reader just keeps going from there. so something like

{"doc": [[[[ ...65 levels... ]]]], "name": "bob"}

hits the depth limit and returns empty, which is fine for doc, but then ReadStruct sees the leftover ], thinks the object is done, and name never gets read. no error either, the data's just missing. pretty niche since you need a 64+ deep document but it's an easy fix to match the other reads

Aws::Crt::Optional<Document> ReadDocument(const Schema&) override {
  const size_t start = m_pos;
  auto doc = ReadDocumentValue(0);
  if (!doc.has_value()) {
    m_pos = start;
    SkipValue();
  }
  return doc;
}

might also be worth adding a member after the document in ReadDocumentRejectsExcessiveNesting, since right now it only checks a document on its own

This branch has not been deployed

No deployments
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.

3 participants