diff --git a/build_tools/services.rb b/build_tools/services.rb
index 5469693679c..3e32e6d8e9b 100644
--- a/build_tools/services.rb
+++ b/build_tools/services.rb
@@ -9,10 +9,10 @@ class ServiceEnumerator
MANIFEST_PATH = File.expand_path('../../services.json', __FILE__)
# Minimum `aws-sdk-core` version for new gem builds
- MINIMUM_CORE_VERSION = "3.255.0"
+ MINIMUM_CORE_VERSION = "3.256.0"
# Minimum `aws-sdk-core` version for new S3 gem builds
- MINIMUM_CORE_VERSION_S3 = "3.255.0"
+ MINIMUM_CORE_VERSION_S3 = "3.256.0"
EVENTSTREAM_PLUGIN = "Aws::Plugins::EventStreamConfiguration"
diff --git a/gems/aws-sdk-core/CHANGELOG.md b/gems/aws-sdk-core/CHANGELOG.md
index 548ce898377..37de7896706 100644
--- a/gems/aws-sdk-core/CHANGELOG.md
+++ b/gems/aws-sdk-core/CHANGELOG.md
@@ -1,6 +1,8 @@
Unreleased Changes
------------------
+* Feature - Preserve millisecond precision when serializing `Time` values on the request path, instead of truncating to whole seconds.
+
3.255.0 (2026-09-09)
------------------
diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/json/builder.rb b/gems/aws-sdk-core/lib/aws-sdk-core/json/builder.rb
index e53a963969c..2775443adb0 100644
--- a/gems/aws-sdk-core/lib/aws-sdk-core/json/builder.rb
+++ b/gems/aws-sdk-core/lib/aws-sdk-core/json/builder.rb
@@ -81,11 +81,11 @@ def encode(blob)
def timestamp(ref, value)
case ref['timestampFormat'] || ref.shape['timestampFormat']
- when 'iso8601' then value.utc.iso8601
+ when 'iso8601' then Util.serialize_date_time(value)
when 'rfc822' then value.utc.httpdate
else
# rest-json and jsonrpc default to unixTimestamp
- value.to_i
+ Util.serialize_epoch_seconds(value)
end
end
diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/query/ec2_param_builder.rb b/gems/aws-sdk-core/lib/aws-sdk-core/query/ec2_param_builder.rb
index e48bfe47bdf..dd901250bc5 100644
--- a/gems/aws-sdk-core/lib/aws-sdk-core/query/ec2_param_builder.rb
+++ b/gems/aws-sdk-core/lib/aws-sdk-core/query/ec2_param_builder.rb
@@ -70,11 +70,11 @@ def blob(value)
def timestamp(ref, value)
case ref['timestampFormat'] || ref.shape['timestampFormat']
- when 'unixTimestamp' then value.to_i
+ when 'unixTimestamp' then Util.serialize_epoch_seconds(value)
when 'rfc822' then value.utc.httpdate
else
# ec2 defaults to iso8601
- value.utc.iso8601
+ Util.serialize_date_time(value)
end
end
diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/query/param_builder.rb b/gems/aws-sdk-core/lib/aws-sdk-core/query/param_builder.rb
index 446e80e17c5..87c57b137a6 100644
--- a/gems/aws-sdk-core/lib/aws-sdk-core/query/param_builder.rb
+++ b/gems/aws-sdk-core/lib/aws-sdk-core/query/param_builder.rb
@@ -87,11 +87,11 @@ def flat?(ref)
def timestamp(ref, value)
case ref['timestampFormat'] || ref.shape['timestampFormat']
- when 'unixTimestamp' then value.to_i
+ when 'unixTimestamp' then Util.serialize_epoch_seconds(value)
when 'rfc822' then value.utc.httpdate
else
# query defaults to iso8601
- value.utc.iso8601
+ Util.serialize_date_time(value)
end
end
diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/rest/request/endpoint.rb b/gems/aws-sdk-core/lib/aws-sdk-core/rest/request/endpoint.rb
index b69ad71b090..f4dd8518df6 100644
--- a/gems/aws-sdk-core/lib/aws-sdk-core/rest/request/endpoint.rb
+++ b/gems/aws-sdk-core/lib/aws-sdk-core/rest/request/endpoint.rb
@@ -66,11 +66,11 @@ def param_name(placeholder)
def timestamp(ref, value)
case ref['timestampFormat']
- when 'unixTimestamp' then value.to_i
+ when 'unixTimestamp' then Util.serialize_epoch_seconds(value)
when 'rfc822' then value.utc.httpdate
else
# serializing as RFC 3399 date-time is the default
- value.utc.iso8601
+ Util.serialize_date_time(value)
end
end
diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/rest/request/headers.rb b/gems/aws-sdk-core/lib/aws-sdk-core/rest/request/headers.rb
index 4d0b088b78a..5f2d68f472d 100644
--- a/gems/aws-sdk-core/lib/aws-sdk-core/rest/request/headers.rb
+++ b/gems/aws-sdk-core/lib/aws-sdk-core/rest/request/headers.rb
@@ -42,8 +42,8 @@ def apply_header_value(headers, ref, value)
def timestamp(ref, value)
case ref['timestampFormat'] || ref.shape['timestampFormat']
- when 'unixTimestamp' then value.to_i
- when 'iso8601' then value.utc.iso8601
+ when 'unixTimestamp' then Util.serialize_epoch_seconds(value)
+ when 'iso8601' then Util.serialize_date_time(value)
else
# header default to rfc822
value.utc.httpdate
diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/rest/request/querystring_builder.rb b/gems/aws-sdk-core/lib/aws-sdk-core/rest/request/querystring_builder.rb
index 8c818e150be..49f1a8a3fe9 100644
--- a/gems/aws-sdk-core/lib/aws-sdk-core/rest/request/querystring_builder.rb
+++ b/gems/aws-sdk-core/lib/aws-sdk-core/rest/request/querystring_builder.rb
@@ -63,11 +63,11 @@ def build_part(shape_ref, param_value, query_keys)
def timestamp(ref, value)
case ref['timestampFormat'] || ref.shape['timestampFormat']
- when 'unixTimestamp' then value.to_i
+ when 'unixTimestamp' then Util.serialize_epoch_seconds(value)
when 'rfc822' then value.utc.httpdate
else
# querystring defaults to iso8601
- value.utc.iso8601
+ Util.serialize_date_time(value)
end
end
diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/util.rb b/gems/aws-sdk-core/lib/aws-sdk-core/util.rb
index db749aabafb..ee0dea174b1 100644
--- a/gems/aws-sdk-core/lib/aws-sdk-core/util.rb
+++ b/gems/aws-sdk-core/lib/aws-sdk-core/util.rb
@@ -91,6 +91,30 @@ def deserialize_number(str)
end
end
+ # Serializes a {Time} to the Smithy `epoch-seconds` format: seconds
+ # since the Unix epoch with optional millisecond precision. Whole-second
+ # values are returned as an Integer so the wire format is unchanged for
+ # the common case; sub-millisecond precision is truncated to milliseconds.
+ # @see https://smithy.io/2.0/spec/protocol-traits.html#timestamp-formats
+ # @param [Time] value
+ # @return [Integer, Float]
+ def serialize_epoch_seconds(value)
+ return value.to_i if value.nsec.zero?
+
+ value.to_i + value.nsec / 1_000_000 / 1000.0
+ end
+
+ # Serializes a {Time} to the Smithy `date-time` format: an RFC 3339
+ # (ISO 8601) string with optional millisecond precision and no UTC
+ # offset. Fractional seconds are included only when present, and
+ # sub-millisecond precision is truncated to milliseconds.
+ # @see https://smithy.io/2.0/spec/protocol-traits.html#timestamp-formats
+ # @param [Time] value
+ # @return [String]
+ def serialize_date_time(value)
+ value.nsec.zero? ? value.utc.iso8601 : value.utc.iso8601(3)
+ end
+
# @param [String] value
# @return [Time]
def deserialize_time(value)
diff --git a/gems/aws-sdk-core/lib/aws-sdk-core/xml/builder.rb b/gems/aws-sdk-core/lib/aws-sdk-core/xml/builder.rb
index 836f56f8327..0813a03a3fb 100644
--- a/gems/aws-sdk-core/lib/aws-sdk-core/xml/builder.rb
+++ b/gems/aws-sdk-core/lib/aws-sdk-core/xml/builder.rb
@@ -106,11 +106,11 @@ def blob(value)
def timestamp(ref, value)
case ref['timestampFormat'] || ref.shape['timestampFormat']
- when 'unixTimestamp' then value.to_i
+ when 'unixTimestamp' then Util.serialize_epoch_seconds(value)
when 'rfc822' then value.utc.httpdate
else
# xml defaults to iso8601
- value.utc.iso8601
+ Util.serialize_date_time(value)
end
end
diff --git a/gems/aws-sdk-core/spec/aws/json/builder_spec.rb b/gems/aws-sdk-core/spec/aws/json/builder_spec.rb
index b1c08fb5199..59a31f09185 100644
--- a/gems/aws-sdk-core/spec/aws/json/builder_spec.rb
+++ b/gems/aws-sdk-core/spec/aws/json/builder_spec.rb
@@ -74,6 +74,13 @@ def json(params)
expect(json(string: 'abc', integer: nil)).to eq('{"String":"abc"}')
end
+ it 'preserves millisecond precision on epoch timestamps' do
+ params = Structure.new(*rules.shape.member_names).new
+ params.timestamp = Time.at(123_456_789, 123_000)
+ # JRuby and MRI render the same float differently but both are valid JSON for the same value
+ expect(Aws::Json.load(json(params))['Timestamp']).to eq(123_456_789.123)
+ end
+
describe 'document types' do
it 'serializes BigDecimal values as JSON numbers' do
expect(json(document_type: {
diff --git a/gems/aws-sdk-core/spec/aws/stubbing/protocols/json_spec.rb b/gems/aws-sdk-core/spec/aws/stubbing/protocols/json_spec.rb
index 5dc874f4b4e..2e1e0888f64 100644
--- a/gems/aws-sdk-core/spec/aws/stubbing/protocols/json_spec.rb
+++ b/gems/aws-sdk-core/spec/aws/stubbing/protocols/json_spec.rb
@@ -72,7 +72,7 @@ def normalize(json)
"AttributeType": "S"
}
],
- "CreationDateTime": #{now.to_i},
+ "CreationDateTime": #{Aws::Util.serialize_epoch_seconds(now)},
"ItemCount": 0,
"KeySchema": [
{
@@ -81,8 +81,8 @@ def normalize(json)
}
],
"ProvisionedThroughput": {
- "LastIncreaseDateTime": #{now.to_i},
- "LastDecreaseDateTime": #{now.to_i},
+ "LastIncreaseDateTime": #{Aws::Util.serialize_epoch_seconds(now)},
+ "LastDecreaseDateTime": #{Aws::Util.serialize_epoch_seconds(now)},
"NumberOfDecreasesToday": 0,
"ReadCapacityUnits": 50,
"WriteCapacityUnits": 50
diff --git a/gems/aws-sdk-core/spec/aws/stubbing/protocols/query_spec.rb b/gems/aws-sdk-core/spec/aws/stubbing/protocols/query_spec.rb
index 7320bd3b41e..b537f891867 100644
--- a/gems/aws-sdk-core/spec/aws/stubbing/protocols/query_spec.rb
+++ b/gems/aws-sdk-core/spec/aws/stubbing/protocols/query_spec.rb
@@ -49,7 +49,7 @@ def normalize(xml)
name
user-id
arn:aws:iam::123456789012:user/name
- #{now.utc.iso8601}
+ #{Aws::Util.serialize_date_time(now)}
false
diff --git a/gems/aws-sdk-core/spec/aws/stubbing/protocols/rest_xml_spec.rb b/gems/aws-sdk-core/spec/aws/stubbing/protocols/rest_xml_spec.rb
index 28e4b30ebb2..90e2092804c 100644
--- a/gems/aws-sdk-core/spec/aws/stubbing/protocols/rest_xml_spec.rb
+++ b/gems/aws-sdk-core/spec/aws/stubbing/protocols/rest_xml_spec.rb
@@ -56,7 +56,7 @@ def normalize(xml)
aws-sdk
- #{now.utc.iso8601}
+ #{Aws::Util.serialize_date_time(now)}
diff --git a/gems/aws-sdk-core/spec/aws/util_spec.rb b/gems/aws-sdk-core/spec/aws/util_spec.rb
index bd7a39bb021..0808253d389 100644
--- a/gems/aws-sdk-core/spec/aws/util_spec.rb
+++ b/gems/aws-sdk-core/spec/aws/util_spec.rb
@@ -4,6 +4,40 @@
module Aws
describe Util do
+ describe '.serialize_epoch_seconds' do
+ it 'returns an Integer for whole-second values' do
+ result = Util.serialize_epoch_seconds(Time.at(123_456_789))
+ expect(result).to eql(123_456_789)
+ end
+
+ it 'preserves millisecond precision for sub-second values' do
+ result = Util.serialize_epoch_seconds(Time.at(1_700_000_000, 123_000))
+ expect(result).to eq(1_700_000_000.123)
+ end
+
+ it 'truncates precision finer than milliseconds' do
+ result = Util.serialize_epoch_seconds(Time.at(1_700_000_000, 123_456))
+ expect(result).to eq(1_700_000_000.123)
+ end
+ end
+
+ describe '.serialize_date_time' do
+ it 'omits fractional seconds for whole-second values' do
+ result = Util.serialize_date_time(Time.at(1_700_000_000))
+ expect(result).to eq('2023-11-14T22:13:20Z')
+ end
+
+ it 'includes millisecond precision for sub-second values' do
+ result = Util.serialize_date_time(Time.at(1_700_000_000, 123_000))
+ expect(result).to eq('2023-11-14T22:13:20.123Z')
+ end
+
+ it 'truncates precision finer than milliseconds' do
+ result = Util.serialize_date_time(Time.at(1_700_000_000, 123_456))
+ expect(result).to eq('2023-11-14T22:13:20.123Z')
+ end
+ end
+
describe '.deserialize_time' do
let(:time) { Time.at(946_845_296.123) }
diff --git a/gems/aws-sdk-glacier/spec/client_spec.rb b/gems/aws-sdk-glacier/spec/client_spec.rb
index 0373a1b24e9..8aa87bb2a81 100644
--- a/gems/aws-sdk-glacier/spec/client_spec.rb
+++ b/gems/aws-sdk-glacier/spec/client_spec.rb
@@ -21,7 +21,7 @@ module Glacier
body = Json.load(resp.context.http_request.body_contents)
time = body['InventoryRetrievalParameters']['StartDate']
expect(time).to be_a(String)
- expect(time).to eq(now.utc.iso8601)
+ expect(time).to eq(Aws::Util.serialize_date_time(now))
end
it 'handles seeked io objects in :upload_archive' do