Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .changelog/1763738215.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
applies_to:
- client
- aws-sdk-rust
authors:
- AmitKulkarni23
references:
- smithy-rs#312
breaking: false
new_feature: true
bug_fix: false
---
Add support for Smithy bigInteger and bigDecimal types as string wrappers in aws-smithy-types, allowing users to parse with their preferred big number library.
1 change: 1 addition & 0 deletions codegen-client-test/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ data class ClientTest(

val allCodegenTests = listOf(
ClientTest("com.amazonaws.simple#SimpleService", "simple", dependsOn = listOf("simple.smithy")),
ClientTest("com.amazonaws.bignumbers#BigNumberService", "big_numbers", dependsOn = listOf("big-numbers.smithy")),
ClientTest("com.amazonaws.dynamodb#DynamoDB_20120810", "dynamo"),
ClientTest("com.amazonaws.ebs#Ebs", "ebs", dependsOn = listOf("ebs.json")),
ClientTest("aws.protocoltests.json10#JsonRpc10", "json_rpc10"),
Expand Down
143 changes: 143 additions & 0 deletions codegen-core/common-test-models/big-numbers.smithy
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
$version: "2.0"

namespace com.amazonaws.bignumbers

use aws.protocols#restJson1
use smithy.test#httpRequestTests
use smithy.test#httpResponseTests

@restJson1
service BigNumberService {
version: "2023-01-01"
operations: [ProcessBigNumbers]
}

@http(uri: "/process", method: "POST")
Copy link
Collaborator

Choose a reason for hiding this comment

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

there appears to be some code that handles E / scientific notation but I don't see any tests of that here

@httpRequestTests([
{
id: "BigNumbersInJsonRequest",
protocol: restJson1,
method: "POST",
uri: "/process",
body: "{\"bigInt\":123456789,\"bigDec\":123.456789}",
bodyMediaType: "application/json",
headers: {"Content-Type": "application/json"},
params: {
bigInt: 123456789,
bigDec: 123.456789
}
},
{
id: "NegativeBigNumbersInJsonRequest",
protocol: restJson1,
method: "POST",
uri: "/process",
body: "{\"bigInt\":-987654321,\"bigDec\":-0.000000001}",
bodyMediaType: "application/json",
headers: {"Content-Type": "application/json"},
params: {
bigInt: -987654321,
bigDec: -0.000000001
}
},
{
id: "ZeroBigNumbersInJsonRequest",
protocol: restJson1,
method: "POST",
uri: "/process",
body: "{\"bigInt\":0,\"bigDec\":0.0}",
bodyMediaType: "application/json",
headers: {"Content-Type": "application/json"},
params: {
bigInt: 0,
bigDec: 0.0
}
},
{
id: "VeryLargeBigNumbersInJsonRequest",
protocol: restJson1,
method: "POST",
uri: "/process",
body: "{\"bigInt\":9007199254740991,\"bigDec\":123456.789}",
bodyMediaType: "application/json",
headers: {"Content-Type": "application/json"},
params: {
bigInt: 9007199254740991,
bigDec: 123456.789
}
}
])
@httpResponseTests([
{
id: "BigNumbersInJsonResponse",
protocol: restJson1,
code: 200,
body: "{\"result\":999999999,\"ratio\":0.123456789}",
bodyMediaType: "application/json",
headers: {"Content-Type": "application/json"},
params: {
result: 999999999,
ratio: 0.123456789
}
},
{
id: "NegativeBigNumbersInJsonResponse",
protocol: restJson1,
code: 200,
body: "{\"result\":-123456789,\"ratio\":-999.999}",
bodyMediaType: "application/json",
headers: {"Content-Type": "application/json"},
params: {
result: -123456789,
ratio: -999.999
}
},
{
id: "VeryLargeBigNumbersInJsonResponse",
protocol: restJson1,
code: 200,
body: "{\"result\":9007199254740991,\"ratio\":123456.789}",
bodyMediaType: "application/json",
headers: {"Content-Type": "application/json"},
params: {
result: 9007199254740991,
ratio: 123456.789
}
},
{
id: "ZeroBigNumbersInJsonResponse",
protocol: restJson1,
code: 200,
body: "{\"result\":0,\"ratio\":0.0}",
bodyMediaType: "application/json",
headers: {"Content-Type": "application/json"},
params: {
result: 0,
ratio: 0.0
}
},
{
id: "NullBigNumbersInJsonResponse",
protocol: restJson1,
code: 200,
body: "{\"result\":null,\"ratio\":null}",
bodyMediaType: "application/json",
headers: {"Content-Type": "application/json"},
params: {}
}
])
operation ProcessBigNumbers {
input: BigNumberInput
output: BigNumberOutput
}
structure BigNumberInput {
bigInt: BigInteger
bigDec: BigDecimal
}
structure BigNumberOutput {
result: BigInteger
ratio: BigDecimal
}
9 changes: 4 additions & 5 deletions codegen-core/common-test-models/misc.smithy
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,11 @@ structure InnermostShape {
@required
aDouble: Double,

// TODO(https://github.com/smithy-lang/smithy-rs/issues/312)
// @required
// aBigInteger: BigInteger,
@required
aBigInteger: BigInteger,

// @required
// aBigDecimal: BigDecimal,
@required
aBigDecimal: BigDecimal,

@required
aTimestamp: Timestamp,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,10 @@ data class RuntimeType(val path: String, val dependency: RustDependency? = null)

fun dateTime(runtimeConfig: RuntimeConfig) = smithyTypes(runtimeConfig).resolve("DateTime")

fun bigInteger(runtimeConfig: RuntimeConfig) = smithyTypes(runtimeConfig).resolve("BigInteger")

fun bigDecimal(runtimeConfig: RuntimeConfig) = smithyTypes(runtimeConfig).resolve("BigDecimal")

fun document(runtimeConfig: RuntimeConfig): RuntimeType = smithyTypes(runtimeConfig).resolve("Document")

fun format(runtimeConfig: RuntimeConfig) = smithyTypes(runtimeConfig).resolve("date_time::Format")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,11 +245,11 @@ open class SymbolVisitor(
}

override fun bigIntegerShape(shape: BigIntegerShape?): Symbol {
TODO("Not yet implemented: https://github.com/smithy-lang/smithy-rs/issues/312")
return RuntimeType.bigInteger(config.runtimeConfig).toSymbol()
}

override fun bigDecimalShape(shape: BigDecimalShape?): Symbol {
TODO("Not yet implemented: https://github.com/smithy-lang/smithy-rs/issues/312")
return RuntimeType.bigDecimal(config.runtimeConfig).toSymbol()
}

override fun operationShape(shape: OperationShape): Symbol {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import software.amazon.smithy.model.node.NullNode
import software.amazon.smithy.model.node.NumberNode
import software.amazon.smithy.model.node.ObjectNode
import software.amazon.smithy.model.node.StringNode
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.CollectionShape
Expand Down Expand Up @@ -544,6 +546,20 @@ class PrimitiveInstantiator(
}

is StringShape -> renderString(shape, data as StringNode)(this)
is BigIntegerShape -> {
val value = data.toString()
rustTemplate(
"<#{BigInteger} as ::std::str::FromStr>::from_str(${value.dq()}).unwrap()",
"BigInteger" to RuntimeType.bigInteger(runtimeConfig),
)
}
is BigDecimalShape -> {
val value = data.toString()
rustTemplate(
"<#{BigDecimal} as ::std::str::FromStr>::from_str(${value.dq()}).unwrap()",
"BigDecimal" to RuntimeType.bigDecimal(runtimeConfig),
)
}
is NumberShape ->
when (data) {
is StringNode -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@

package software.amazon.smithy.rust.codegen.core.smithy.protocols.parse

import software.amazon.smithy.codegen.core.CodegenException
import software.amazon.smithy.codegen.core.Symbol
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
Expand Down Expand Up @@ -579,6 +582,18 @@ class CborParserGenerator(

is TimestampShape -> rust("decoder.timestamp()")

// BigInteger/BigDecimal are not supported with CBOR.
// The Smithy RPC v2 CBOR spec requires these to be encoded using CBOR tags 2/3/4
// (binary bignum representation), but aws-smithy-cbor doesn't implement these tags yet.
is BigIntegerShape ->
Copy link
Author

Choose a reason for hiding this comment

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

Note to reviewers:

I have added serialization and parsing logic for the following protocols:

  • JSON
  • CBOR
  • XML
  • AWS Query
  • AWS EC2

Let me know if there are any other protocols.

throw CodegenException(
"BigInteger is not supported with Concise Binary Object Representation (CBOR) protocol",
)
is BigDecimalShape ->
throw CodegenException(
"BigDecimal is not supported with Concise Binary Object Representation (CBOR) protocol",
)

// Aggregate shapes: https://smithy.io/2.0/spec/aggregate-types.html
is StructureShape -> deserializeStruct(target)
is CollectionShape -> deserializeCollection(target)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
package software.amazon.smithy.rust.codegen.core.smithy.protocols.parse

import software.amazon.smithy.codegen.core.Symbol
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.CollectionShape
Expand Down Expand Up @@ -296,6 +298,8 @@ class JsonParserGenerator(
when (val target = model.expectShape(memberShape.target)) {
is StringShape -> deserializeString(target)
is BooleanShape -> rustTemplate("#{expect_bool_or_null}(tokens.next())?", *codegenScope)
is BigIntegerShape -> deserializeBigInteger()
Copy link
Collaborator

Choose a reason for hiding this comment

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

we probably need to support this for more than just json protocols. also need protocol tests. does smithy have any protocol tests for these yet?

Copy link
Author

Choose a reason for hiding this comment

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

Ok. I will add serialization and deserialization code for XML, CBOR protocols in the next revision.

Copy link
Author

@AmitKulkarni23 AmitKulkarni23 Nov 24, 2025

Choose a reason for hiding this comment

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

does smithy have any protocol tests for these yet?

Protocol tests exist in misc.smithy but BigInteger/BigDecimal are commented out - https://github.com/smithy-lang/smithy-rs/blob/main/codegen-core/common-test-models/misc.smithy#L100. I will uncomment them now that the implementation is complete. However, it seems like misc.smithy only tests JSON. I will look at references and add protocol tests.

Copy link
Author

Choose a reason for hiding this comment

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

Addressed this in the latest revision of PR

is BigDecimalShape -> deserializeBigDecimal()
is NumberShape -> deserializeNumber(target)
is BlobShape -> deserializeBlob(memberShape)
is TimestampShape -> deserializeTimestamp(memberShape)
Expand Down Expand Up @@ -374,6 +378,63 @@ class JsonParserGenerator(
}
}

private fun RustWriter.deserializeBigInteger() {
// Match on Number enum to:
// 1. Validate only integers are accepted (reject floats)
// 2. Extract inner value and convert to string

rustTemplate(
"""
#{expect_number_or_null}(tokens.next())?
Copy link
Collaborator

Choose a reason for hiding this comment

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

I think do actually do this properly you need to add some additional code to aws-smithy-json to parse a number as a string? not sure how hard that would be.

As it is, this isn't terrible, but its not ideal since it defeats the point

.map(|v| {
let s = match v {
#{Number}::PosInt(n) => n.to_string(),
#{Number}::NegInt(n) => n.to_string(),
#{Number}::Float(_) => return Err(#{Error}::custom("expected integer, found float")),
};
Ok(<#{BigInteger} as ::std::str::FromStr>::from_str(&s).expect("infallible"))
})
.transpose()?
""",
"BigInteger" to RuntimeType.bigInteger(codegenContext.runtimeConfig),
"Number" to RuntimeType.smithyTypes(codegenContext.runtimeConfig).resolve("Number"),
*codegenScope,
)
}

private fun RustWriter.deserializeBigDecimal() {
// Match on Number enum to extract inner value and convert to string
// (Number doesn't implement Display, so we must match each variant)
// For floats, preserve decimal notation that f64::to_string() drops for whole numbers

rustTemplate(
"""
#{expect_number_or_null}(tokens.next())?
.map(|v| {
let s = match v {
#{Number}::PosInt(n) => n.to_string(),
#{Number}::NegInt(n) => n.to_string(),
#{Number}::Float(f) => {
// Use format! to avoid scientific notation and preserve precision
let s = format!("{f}");
// f64 formatting drops ".0" for whole numbers (0.0 -> "0")
// Restore it to preserve that the original JSON had decimal notation
if !s.contains('.') && !s.contains('e') && !s.contains('E') {
format!("{s}.0")
} else {
s
}
Comment on lines +419 to +426
Copy link
Collaborator

Choose a reason for hiding this comment

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

is this correct? I definitely want to see some tests for this.

},
};
<#{BigDecimal} as ::std::str::FromStr>::from_str(&s).expect("infallible")
})
""",
"BigDecimal" to RuntimeType.bigDecimal(codegenContext.runtimeConfig),
"Number" to RuntimeType.smithyTypes(codegenContext.runtimeConfig).resolve("Number"),
*codegenScope,
)
}

private fun RustWriter.deserializeTimestamp(member: MemberShape) {
val timestampFormat =
httpBindingResolver.timestampFormat(
Expand Down
Loading