🚀 New: chi (χ) — an open-source autoresearch harness for fleets of LLM coding agents. Read the announcement.

AWS's non_exhaustive Rust Enum Wasn't the Breaking Change. The Type Inside It Was

On September 14, aws-smithy-types 1.7.0 shipped Document as #[non_exhaustive] and broke cargo builds across the AWS SDK for Rust ecosystem within a day, forcing a full yank and revert to 1.6.4 by September 16. The non_exhaustive marker gets the blame in the bug reports, but the actual break was a second, unrelated change bundled into the same minor version: Document::Object's inner type moved from HashMap to an ordered map. What each change really broke, and why cargo's resolver couldn't stop it.

Contents

On September 14, AWS shipped aws-smithy-types 1.7.0, a minor version bump to the low-level crate that every AWS SDK for Rust client depends on. The next day, a user filed smithy-lang/smithy-rs#4853: a clean cargo add aws-config on a fresh project failed to compile, with aws-smithy-json 0.62.4 and 0.63.0 both throwing type errors against the new release. By September 16, AWS had yanked 1.7.0 from crates.io entirely and shipped 1.6.4, restoring the original API.[1][2][3] Two days from breaking release to full revert is fast for a dependency this central, and the postmortem-shaped detail is in the diff: the bug reports all point at one change, #[non_exhaustive], but that wasn’t what broke the build. A second, unrelated change riding in the same release was.

What 1.7.0 actually changed

The release implemented a Smithy Enhancement Proposal called “Document Types and Type Registries,” merged as smithy-rs PR #4721 by landonxjames on September 14. Document, the enum AWS SDKs use to represent untyped JSON-like values, gained four variants it didn’t have before: Blob(Vec<u8>), Timestamp(DateTime), BigInteger, and BigDecimal. To let future variants be added the same way without another breaking release, the enum was marked #[non_exhaustive], which requires any external match on Document to carry a wildcard arm.[1][3]

That part is the change everyone expected and the one the release notes led with. The part that actually broke builds was a second change bundled into the same commit: Document::Object, previously HashMap<String, Document>, now wraps a new type, aws_smithy_types::document::DocumentObject, backed by indexmap instead of std::collections::HashMap. The motivation was real: Smithy protocols that care about field order in serialized output need documents to preserve insertion order, which a HashMap never guaranteed. But it means any code that pattern-matched Document::Object(map) and then called HashMap-only methods on map, or that constructed Document::Object(HashMap::new()) directly, stopped compiling. That has nothing to do with #[non_exhaustive]. It would have broken exactly the same way if Document had stayed a plain, exhaustive enum.[1][4]

Two breaks, one issue thread

The exact failures in #4853 show both mechanisms firing independently. In aws-smithy-json 0.62.4, src/serialize.rs:36 failed with E0004: non-exhaustive patterns, the #[non_exhaustive] break. In the same crate, src/deserialize/token.rs:323 failed with E0308: mismatched types, expected DocumentObject, found HashMap, the type-change break. aws-smithy-json 0.63.0 hit a third instance of the same E0308 at src/codec/deserializer.rs:707. Two different compiler errors, two different root causes, one dependency bump.[1] Conflating them matters in practice: a team hitting this could reasonably patch only their exhaustive matches, add the wildcard arm the compiler asked for, and still have a broken build wherever they touched Document::Object’s contents directly.

Why cargo let a formally valid dependency break

Both affected aws-smithy-json releases declared compatible ranges on aws-smithy-types: 0.62.4 required >=1.4.4, <2.0.0, and 0.63.0 required >=1.6.1, <2.0.0. By ordinary semver, 1.7.0 satisfies both. Cargo did exactly what it’s supposed to do: for a single build, it resolves one shared version of aws-smithy-types across the whole dependency graph wherever version ranges overlap, rather than letting each crate carry its own copy. aws-config 1.12.0 still required aws-smithy-json ^0.63.0, so a fresh cargo add aws-config could legally pull in both 0.62.4 and 0.63.0 as separate aws-smithy-json instances (0.x releases aren’t semver-compatible with each other), but both of those instances resolved against the same single aws-smithy-types 1.7.0. Neither aws-smithy-json release had done anything wrong by the version ranges it declared. The version that broke them satisfied every range in the graph.[1]

That’s the actual failure mode, and it’s not specific to AWS: an open-ended ^1.x dependency on a crate sitting at a fan-in point in the graph means every transitive consumer inherits whatever that crate’s next 1.x release does, and cargo’s resolver has no way to distinguish a minor version that’s honestly additive from one that isn’t, because it trusts the number the publisher chose. The Rust API guidelines are explicit that adding #[non_exhaustive] to a previously exhaustive public enum, and changing an existing variant’s payload type, are both breaking changes that call for a major version bump before 1.0 stabilizes further, or a new major release after it.[1] AWS shipped both as 1.6.3 to 1.7.0.

What actually shipped as the fix

aws-smithy-types 1.6.4, released September 16, is a straightforward revert: Document goes back to six variants (Object, Array, Number, String, Bool, Null), Object goes back to wrapping HashMap<String, Document>, and the #[non_exhaustive] marker is gone. AWS states plainly that code written against 1.6.3 needs no changes, and because 1.6.4 still satisfies every ^1.x range in the ecosystem, cargo resolves back to it automatically on the next cargo update.[3] The Document Types and Type Registries work itself wasn’t abandoned, just pulled back out of the 1.x line to be reintroduced without breaking the crate that half the Rust AWS ecosystem depends on transitively.

The decision this changes

If you maintain a widely-depended-on crate and you’re about to mark a previously exhaustive public enum #[non_exhaustive], that’s one breaking change, worth its own release and its own version bump. If you’re also changing what an existing variant carries, that’s a second, independent breaking change, and bundling it into the same release doesn’t make it free just because the first change was properly telegraphed. Test each change against your semver policy separately, not as one commit.

If you consume a crate like this, the lesson isn’t “pin every dependency,” which trades this failure mode for slower security patching. It’s narrower: for a dependency that sits at a fan-in point in your graph, the guarantee you’re actually relying on is the publisher’s semver discipline, not cargo’s ability to enforce it, because cargo has no way to check that a 1.6.3 to 1.7.0 bump doesn’t change a type signature. Cargo.lock in CI limits exposure to the moment you run cargo update, but it doesn’t remove the dependency on the publisher getting the version number right.

Sources

[1] https://github.com/smithy-lang/smithy-rs/issues/4853: “aws-smithy-types 1.7.0 breaks released aws-smithy-json 0.62.4 and 0.63.0,” opened September 15, 2026

[2] https://github.com/awslabs/aws-sdk-rust/releases/tag/release-2026-09-16: AWS SDK for Rust release notes, September 16, 2026

[3] https://github.com/awslabs/aws-sdk-rust/releases/tag/release-2026-09-14: AWS SDK for Rust release notes, September 14, 2026

[4] https://github.com/smithy-lang/smithy-rs/pull/4721: “Document Type Updates and Type/Error Registries,” landonxjames, merged September 14, 2026