diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/MemberErrorCorrectionGenerator.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/MemberErrorCorrectionGenerator.java
new file mode 100644
index 000000000..e094a82d7
--- /dev/null
+++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/MemberErrorCorrectionGenerator.java
@@ -0,0 +1,212 @@
+/*
+ * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+ * SPDX-License-Identifier: Apache-2.0
+ */
+package software.amazon.smithy.python.codegen.generators;
+
+import software.amazon.smithy.model.shapes.BigDecimalShape;
+import software.amazon.smithy.model.shapes.BigIntegerShape;
+import software.amazon.smithy.model.shapes.BlobShape;
+import software.amazon.smithy.model.shapes.BooleanShape;
+import software.amazon.smithy.model.shapes.ByteShape;
+import software.amazon.smithy.model.shapes.DocumentShape;
+import software.amazon.smithy.model.shapes.DoubleShape;
+import software.amazon.smithy.model.shapes.EnumShape;
+import software.amazon.smithy.model.shapes.FloatShape;
+import software.amazon.smithy.model.shapes.IntEnumShape;
+import software.amazon.smithy.model.shapes.IntegerShape;
+import software.amazon.smithy.model.shapes.ListShape;
+import software.amazon.smithy.model.shapes.LongShape;
+import software.amazon.smithy.model.shapes.MapShape;
+import software.amazon.smithy.model.shapes.MemberShape;
+import software.amazon.smithy.model.shapes.Shape;
+import software.amazon.smithy.model.shapes.ShapeVisitor;
+import software.amazon.smithy.model.shapes.ShortShape;
+import software.amazon.smithy.model.shapes.StringShape;
+import software.amazon.smithy.model.shapes.StructureShape;
+import software.amazon.smithy.model.shapes.TimestampShape;
+import software.amazon.smithy.model.shapes.UnionShape;
+import software.amazon.smithy.model.traits.StreamingTrait;
+import software.amazon.smithy.python.codegen.GenerationContext;
+import software.amazon.smithy.python.codegen.SymbolProperties;
+import software.amazon.smithy.python.codegen.writer.PythonWriter;
+import software.amazon.smithy.utils.SmithyInternalApi;
+
+/**
+ * Emits the Python expression used to fill a missing required member during client error
+ * correction.
+ *
+ * @see Smithy
+ * spec: Client error correction
+ */
+@SmithyInternalApi
+public final class MemberErrorCorrectionGenerator extends ShapeVisitor.DataShapeVisitor {
+
+ private final GenerationContext context;
+ private final PythonWriter writer;
+
+ public MemberErrorCorrectionGenerator(GenerationContext context, PythonWriter writer) {
+ this.context = context;
+ this.writer = writer;
+ }
+
+ /**
+ * @return {@code true} if the visitor will emit a default expression for this shape.
+ */
+ public static boolean hasDefault(Shape target) {
+ return switch (target.getType()) {
+ // Note on streaming shapes:
+ // - Streaming unions (event streams) are filtered out earlier by
+ // StructureGenerator#filterEventStreamMember and never reach this visitor,
+ // so UNION can unconditionally return true here.
+ // - Streaming blobs are NOT filtered earlier, so we explicitly exclude them
+ // below. Per Smithy spec § 13.3.1, a missing streaming blob is already
+ // handled by the deserializer (an empty HTTP body becomes a zero-length
+ // AsyncBytesReader), so client error correction is unnecessary.
+ case BOOLEAN, BYTE, SHORT, INTEGER, LONG, BIG_INTEGER, FLOAT, DOUBLE, BIG_DECIMAL,
+ STRING, TIMESTAMP, DOCUMENT, LIST, MAP, ENUM, INT_ENUM, STRUCTURE, UNION ->
+ true;
+ case BLOB -> !target.hasTrait(StreamingTrait.class);
+ default -> false;
+ };
+ }
+
+ @Override
+ public Boolean booleanShape(BooleanShape shape) {
+ writer.writeInline("False");
+ return true;
+ }
+
+ @Override
+ public Boolean byteShape(ByteShape shape) {
+ writer.writeInline("0");
+ return true;
+ }
+
+ @Override
+ public Boolean shortShape(ShortShape shape) {
+ writer.writeInline("0");
+ return true;
+ }
+
+ @Override
+ public Boolean integerShape(IntegerShape shape) {
+ writer.writeInline("0");
+ return true;
+ }
+
+ @Override
+ public Boolean longShape(LongShape shape) {
+ writer.writeInline("0");
+ return true;
+ }
+
+ @Override
+ public Boolean bigIntegerShape(BigIntegerShape shape) {
+ writer.writeInline("0");
+ return true;
+ }
+
+ @Override
+ public Boolean floatShape(FloatShape shape) {
+ writer.writeInline("0.0");
+ return true;
+ }
+
+ @Override
+ public Boolean doubleShape(DoubleShape shape) {
+ writer.writeInline("0.0");
+ return true;
+ }
+
+ @Override
+ public Boolean bigDecimalShape(BigDecimalShape shape) {
+ writer.addStdlibImport("decimal", "Decimal");
+ writer.writeInline("Decimal(0)");
+ return true;
+ }
+
+ @Override
+ public Boolean stringShape(StringShape shape) {
+ writer.writeInline("\"\"");
+ return true;
+ }
+
+ @Override
+ public Boolean blobShape(BlobShape shape) {
+ writer.writeInline("b\"\"");
+ return true;
+ }
+
+ @Override
+ public Boolean timestampShape(TimestampShape shape) {
+ writer.addStdlibImport("datetime", "datetime");
+ writer.addStdlibImport("datetime", "timezone");
+ writer.writeInline("datetime.fromtimestamp(0, tz=timezone.utc)");
+ return true;
+ }
+
+ @Override
+ public Boolean documentShape(DocumentShape shape) {
+ writer.addImport("smithy_core.documents", "Document");
+ writer.writeInline("Document(None)");
+ return true;
+ }
+
+ @Override
+ public Boolean listShape(ListShape shape) {
+ writer.writeInline("[]");
+ return true;
+ }
+
+ @Override
+ public Boolean mapShape(MapShape shape) {
+ writer.writeInline("{}");
+ return true;
+ }
+
+ @Override
+ public Boolean enumShape(EnumShape shape) {
+ // TODO: the Smithy spec recommends enum types define an unknown variant. If a
+ // future change adds an unknown variant to the generated enum class (e.g.
+ // MyEnum.unknown(value)), revisit this to emit it instead of the bare "".
+ writer.writeInline("\"\"");
+ return true;
+ }
+
+ @Override
+ public Boolean intEnumShape(IntEnumShape shape) {
+ // TODO: the Smithy spec recommends intEnum types define an unknown variant. If a
+ // future change adds an unknown variant to the generated intEnum class (e.g.
+ // MyIntEnum.unknown(value)), revisit this to emit it instead of the bare 0.
+ writer.writeInline("0");
+ return true;
+ }
+
+ @Override
+ public Boolean unionShape(UnionShape shape) {
+ var unknownSymbol = context.symbolProvider()
+ .toSymbol(shape)
+ .expectProperty(SymbolProperties.UNION_UNKNOWN);
+ writer.addImport(unknownSymbol, unknownSymbol.getName());
+ writer.writeInline("$L(tag=\"\")", unknownSymbol.getName());
+ return true;
+ }
+
+ @Override
+ public Boolean structureShape(StructureShape shape) {
+ // Delegate to the target struct's _smithy_default() so nested required fields are
+ // also filled in. Recursion terminates because Smithy forbids recursive paths whose
+ // members are all @required:
+ // https://smithy.io/2.0/spec/aggregate-types.html#recursive-shape-definitions
+ var symbol = context.symbolProvider().toSymbol(shape);
+ writer.addImport(symbol, symbol.getName());
+ writer.writeInline("$L._smithy_default()", symbol.getName());
+ return true;
+ }
+
+ @Override
+ public Boolean memberShape(MemberShape shape) {
+ return context.model().expectShape(shape.getTarget()).accept(this);
+ }
+}
diff --git a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/StructureGenerator.java b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/StructureGenerator.java
index 55d4daf67..62377697c 100644
--- a/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/StructureGenerator.java
+++ b/codegen/core/src/main/java/software/amazon/smithy/python/codegen/generators/StructureGenerator.java
@@ -104,12 +104,15 @@ class $L:
${C|}
+ ${C|}
+
""",
symbol.getName(),
writer.consumer(w -> writeClassDocs()),
writer.consumer(w -> writeProperties()),
writer.consumer(w -> generateSerializeMethod()),
- writer.consumer(w -> generateDeserializeMethod()));
+ writer.consumer(w -> generateDeserializeMethod()),
+ writer.consumer(w -> generateSmithyDefaultMethod()));
}
private void renderError() {
@@ -147,6 +150,8 @@ class $1L($2T):
${7C|}
+ ${8C|}
+
""",
symbol.getName(),
baseError,
@@ -154,7 +159,8 @@ class $1L($2T):
writer.consumer(w -> writeClassDocs()),
writer.consumer(w -> writeProperties()),
writer.consumer(w -> generateSerializeMethod()),
- writer.consumer(w -> generateDeserializeMethod()));
+ writer.consumer(w -> generateDeserializeMethod()),
+ writer.consumer(w -> generateSmithyDefaultMethod()));
}
private void writeClassDocs() {
@@ -375,14 +381,78 @@ def _consumer(schema: Schema, de: ShapeDeserializer) -> None:
logger.debug("Unexpected member schema: %s", schema)
deserializer.read_struct($T, consumer=_consumer)
+ ${C|}
return kwargs
""",
writer.consumer(w -> deserializeMembers(shape.members())),
- schemaSymbol);
+ schemaSymbol,
+ writer.consumer(w -> writeErrorCorrection()));
writer.popState();
}
+ /**
+ * Emits client error correction for required members the server failed to serialize.
+ *
+ * @see Smithy
+ * spec: Client error correction
+ */
+ private void writeErrorCorrection() {
+ var visitor = new MemberErrorCorrectionGenerator(context, writer);
+ for (MemberShape member : requiredMembers) {
+ var target = model.expectShape(member.getTarget());
+ if (!MemberErrorCorrectionGenerator.hasDefault(target)) {
+ // Streaming shapes have no synthesizable default; let the dataclass raise.
+ continue;
+ }
+ writer.pushState();
+ writer.putContext("memberName", symbolProvider.toMemberName(member));
+ writer.write("""
+ if ${memberName:S} not in kwargs:
+ kwargs[${memberName:S}] = ${C|}""",
+ writer.consumer(w -> target.accept(visitor)));
+ writer.popState();
+ }
+ }
+
+ /**
+ * Emits a {@code _smithy_default()} classmethod that constructs an instance with all
+ * required members filled in via client error correction. Used to fill nested structure
+ * members per the Smithy spec. If the structure has any required member whose target has
+ * no synthesizable default (i.e. a streaming blob), {@code _smithy_default()} is omitted:
+ * a generated {@code cls()} call would be missing a required argument. But such structures
+ * can only appear as a top-level operation input or output (per spec § 13.3), never as a
+ * nested-struct member, so {@code _smithy_default()} would never be invoked on them anyway.
+ */
+ private void generateSmithyDefaultMethod() {
+ for (MemberShape member : requiredMembers) {
+ var target = model.expectShape(member.getTarget());
+ if (!MemberErrorCorrectionGenerator.hasDefault(target)) {
+ return;
+ }
+ }
+ writer.write("""
+ @classmethod
+ def _smithy_default(cls) -> Self:
+ return cls(${C|})
+ """,
+ writer.consumer(w -> writeSmithyDefaultArguments()));
+ }
+
+ private void writeSmithyDefaultArguments() {
+ var visitor = new MemberErrorCorrectionGenerator(context, writer);
+ var first = true;
+ for (MemberShape member : requiredMembers) {
+ var target = model.expectShape(member.getTarget());
+ if (!first) {
+ writer.writeInline(", ");
+ }
+ first = false;
+ writer.writeInline("$L=", symbolProvider.toMemberName(member));
+ target.accept(visitor);
+ }
+ }
+
private void deserializeMembers(Collection members) {
int index = -1;
for (MemberShape member : members) {