Skip to main content

aws_smithy_json/
codec.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! JSON codec implementation for schema-based serialization.
7
8use aws_smithy_schema::codec::Codec;
9use aws_smithy_schema::extension::SchemaExtensionKey;
10use aws_smithy_schema::{shape_id, Schema, ShapeId};
11use aws_smithy_types::date_time::{DateTime, Format as TimestampFormat};
12use aws_smithy_types::{DocumentError, DocumentSettings, Number};
13use std::sync::Arc;
14
15mod deserializer;
16mod serializer;
17
18pub use deserializer::JsonDeserializer;
19pub use serializer::JsonSerializer;
20
21/// The JSON object keys (`"name":`) for one member schema, quoted and escaped once
22/// and cached on the schema by [`JSON_MEMBER_KEYS`].
23///
24/// Both [`JsonFieldMapper`] variants are cached because a schema is shared by every
25/// codec in the process, and codecs that honor `@jsonName` and codecs that ignore it
26/// can serialize the same shape.
27#[derive(Debug)]
28struct JsonMemberKeys {
29    /// `"<member name>":`
30    member_name: Box<str>,
31    /// `"<@jsonName value>":`, present only when the member has `@jsonName`.
32    json_name: Option<Box<str>>,
33}
34
35/// Quotes and escapes `name` as a JSON object key, including the trailing colon.
36fn encode_key(name: &str) -> Box<str> {
37    let escaped = crate::escape::escape_string(name);
38    let mut key = String::with_capacity(escaped.len() + 3);
39    key.push('"');
40    key.push_str(&escaped);
41    key.push_str("\":");
42    key.into_boxed_str()
43}
44
45/// Caches a member schema's JSON object keys, so the serializer writes each field
46/// name with one `push_str` instead of quoting and escaping it on every request.
47///
48/// Only requested for schemas that have a member name; see
49/// [`JsonFieldMapper::member_key`].
50static JSON_MEMBER_KEYS: SchemaExtensionKey<JsonMemberKeys> =
51    SchemaExtensionKey::new(|member| JsonMemberKeys {
52        member_name: encode_key(member.member_name().unwrap_or_default()),
53        json_name: member
54            .json_name()
55            .map(|json_name| encode_key(json_name.value())),
56    });
57
58/// Maps between Smithy member names and JSON wire field names.
59///
60/// When `@jsonName` is enabled, the wire name may differ from the member name.
61/// This type handles the mapping in both directions.
62///
63/// The mapper itself is a fieldless `Copy` enum. Encoded keys for serialization are
64/// cached on the member schemas (see [`JsonFieldMapper::member_key`]); reverse
65/// lookups are a linear scan over the struct's members (see
66/// [`JsonFieldMapper::field_to_member`]).
67#[derive(Debug, Clone, Copy)]
68enum JsonFieldMapper {
69    /// Uses member names directly, ignoring `@jsonName`.
70    UseMemberName,
71    /// Uses `@jsonName` trait values when present, falling back to member name.
72    UseJsonName,
73}
74
75impl JsonFieldMapper {
76    /// Returns the encoded JSON object key (`"name":`) for a member schema, or `None`
77    /// if the schema is not a member.
78    #[inline]
79    fn member_key<'s>(&self, member: &'s Schema<'_>) -> Option<&'s str> {
80        // Checked first so that schemas without a field name, such as a top-level
81        // structure, never allocate extension storage.
82        member.member_name()?;
83        let keys = member.extension(&JSON_MEMBER_KEYS);
84        Some(match self {
85            JsonFieldMapper::UseMemberName => &keys.member_name,
86            JsonFieldMapper::UseJsonName => keys.json_name.as_ref().unwrap_or(&keys.member_name),
87        })
88    }
89
90    /// Resolves a JSON wire field name to a member schema within a struct schema.
91    ///
92    /// Returns the first member whose wire name matches, so a field name shared by
93    /// several members (a member name equal to another member's `@jsonName`) always
94    /// resolves to the same member.
95    ///
96    /// This is `O(M)` in the number of struct members and is called once per field
97    /// present on the wire, so deserializing a struct is `O(M²)` in the worst case.
98    /// An unknown field costs a full scan before returning `None`.
99    ///
100    /// **Both** arms scan: `UseJsonName` loops below, and `UseMemberName` delegates
101    /// to `Schema::member_schema`, which is itself a `find` over the members.
102    ///
103    /// Real member counts across AWS models are p50=2, p90=6, p99=20, so this is
104    /// not measurable for the large majority of structs.
105    fn field_to_member<'s>(
106        &self,
107        schema: &'s Schema<'s>,
108        field_name: &str,
109    ) -> Option<&'s Schema<'s>> {
110        match self {
111            JsonFieldMapper::UseMemberName => schema.member_schema(field_name),
112            JsonFieldMapper::UseJsonName => {
113                for member in schema.members() {
114                    if let Some(jn) = member.json_name() {
115                        if jn.value() == field_name {
116                            return Some(member);
117                        }
118                    } else if member.member_name() == Some(field_name) {
119                        return Some(member);
120                    }
121                }
122                None
123            }
124        }
125    }
126}
127
128/// Configuration for JSON codec behavior.
129///
130/// Use the builder methods to construct settings:
131/// ```
132/// use aws_smithy_json::codec::JsonCodecSettings;
133///
134/// let settings = JsonCodecSettings::builder()
135///     .use_json_name(false)
136///     .build();
137/// ```
138///
139/// # As `DocumentSettings`
140///
141/// `JsonCodecSettings` implements [`DocumentSettings`]. The
142/// [`JsonDeserializer`] attaches `Arc<JsonCodecSettings>` to every
143/// document it produces so the format-aware accessors
144/// ([`DiscriminatedDocument::as_blob`](aws_smithy_types::DiscriminatedDocument::as_blob),
145/// [`DiscriminatedDocument::as_timestamp`](aws_smithy_types::DiscriminatedDocument::as_timestamp))
146/// can coerce JSON-encoded blobs (base64 strings) and timestamps
147/// (date-time strings or epoch-seconds numbers) back to typed Rust
148/// values. The same instance is reused on the serialization side so a
149/// document round-trips through `JsonDeserializer` →
150/// `JsonSerializer` losslessly.
151#[derive(Debug, Clone)]
152pub struct JsonCodecSettings {
153    field_mapper: JsonFieldMapper,
154    default_timestamp_format: TimestampFormat,
155    max_depth: u32,
156    /// When `true`, a timestamp must use exactly the wire form its resolved
157    /// `@timestampFormat` (or the codec default) prescribes: a JSON number for
158    /// `epoch-seconds`, an RFC 3339 string without a UTC offset for `date-time`,
159    /// and an IMF-fixdate string for `http-date`. Any other form is rejected.
160    /// When `false` (default) the deserializer is tolerant: a number is always
161    /// read as epoch seconds and a string for a `date-time` or `epoch-seconds`
162    /// member is parsed as an offset-aware `date-time`.
163    strict_timestamp_formats: bool,
164    dispatch_null_members: bool,
165    enforce_strictness: bool,
166    validate_skipped_values: bool,
167    validate_skipped_string_encoding: bool,
168    allow_integral_float_numbers: bool,
169    allow_leading_zeros: bool,
170    allow_trailing_decimal_point: bool,
171    allow_empty_integer_part: bool,
172    /// Identifies the protocol that produced this codec — used by
173    /// `DocumentSettings` for diagnostics on coercion failures.
174    /// Default: `aws.smithy.json#JsonCodec`. AWS protocols (awsJson1_0,
175    /// awsJson1_1, restJson1) override this to their own shape id.
176    protocol_id: ShapeId<'static>,
177    /// When `true`, `bigInteger` and `bigDecimal` shapes serialize as
178    /// JSON strings to preserve precision for receivers using
179    /// `f64`-routed JSON parsers. When `false` (default), they emit as
180    /// raw JSON numbers — interoperable with arbitrary-precision JSON
181    /// parsers but lossy when the receiver routes through `f64`.
182    ///
183    /// The read path always accepts both wire forms, regardless of
184    /// this setting, so a sender configured for one form interoperates
185    /// with a receiver configured for the other.
186    use_string_for_arbitrary_precision: bool,
187    /// When `true`, a `bigInteger` or `bigDecimal` whose text is a valid decimal but not an
188    /// RFC 8259 JSON number (a leading `+`, leading zeros, `.5`, `1.`) is rewritten to the
189    /// equivalent JSON number on the raw-number path. When `false` (default) it is rejected with
190    /// [`SerdeError::InvalidInput`](aws_smithy_schema::serde::SerdeError). Has no effect when
191    /// `use_string_for_arbitrary_precision` is set.
192    normalize_arbitrary_precision: bool,
193    /// Default Smithy namespace used to resolve relative shape IDs in
194    /// JSON `__type` discriminator fields produced by services that
195    /// emit relative names (e.g., awsJson1_0/1_1 senders).
196    ///
197    /// When set, [`crate::codec::deserializer::JsonDeserializer::read_discriminated_document`]
198    /// resolves a relative `__type` value (e.g., `"Capacity"`) to its
199    /// fully-qualified shape ID (`"<default_namespace>#Capacity"`)
200    /// before lifting it onto the resulting [`DiscriminatedDocument`](aws_smithy_types::DiscriminatedDocument)
201    /// discriminator slot. Absolute `__type` values (already
202    /// containing `#`) are taken as-is regardless of this setting.
203    ///
204    /// `None` (default) preserves the prior behavior where relative
205    /// names are left in the resulting map as plain string entries.
206    default_namespace: Option<String>,
207}
208
209impl JsonCodecSettings {
210    /// Whether null structure members are passed to the consumer instead of skipped.
211    /// Schema servers use this to clear optional fields while leaving required validation to builders.
212    pub fn dispatch_null_members(&self) -> bool {
213        self.dispatch_null_members
214    }
215
216    /// Creates a builder for `JsonCodecSettings`.
217    pub fn builder() -> JsonCodecSettingsBuilder {
218        JsonCodecSettingsBuilder::default()
219    }
220
221    /// Default timestamp format when not specified by `@timestampFormat` trait.
222    pub fn default_timestamp_format(&self) -> TimestampFormat {
223        self.default_timestamp_format
224    }
225
226    /// Maximum aggregate nesting depth the deserializer will accept before
227    /// returning an error. Defends against stack overflow on recursive shapes
228    /// and deeply-nested document payloads.
229    pub fn max_depth(&self) -> u32 {
230        self.max_depth
231    }
232
233    /// Whether timestamps must use exactly the wire form their format prescribes.
234    /// See [`JsonCodecSettingsBuilder::strict_timestamp_formats`].
235    pub fn strict_timestamp_formats(&self) -> bool {
236        self.strict_timestamp_formats
237    }
238
239    /// Whether `bigInteger` and `bigDecimal` shapes emit as JSON
240    /// strings (when `true`) or as raw JSON numbers
241    /// (when `false`, the default). The read path is always lenient —
242    /// `read_big_integer` and `read_big_decimal` accept either wire form.
243    pub fn use_string_for_arbitrary_precision(&self) -> bool {
244        self.use_string_for_arbitrary_precision
245    }
246
247    /// Whether non-JSON-number arbitrary-precision text is rewritten to the equivalent JSON
248    /// number (`true`) or rejected (`false`, the default). See the field documentation.
249    pub fn normalize_arbitrary_precision(&self) -> bool {
250        self.normalize_arbitrary_precision
251    }
252
253    /// Default Smithy namespace used to resolve relative shape IDs in
254    /// JSON `__type` discriminator fields. `None` if unset.
255    ///
256    /// See the field-level doc comment for [`Self::default_namespace`]'s
257    /// builder method for the resolution behavior.
258    pub fn default_namespace(&self) -> Option<&str> {
259        self.default_namespace.as_deref()
260    }
261
262    /// Returns a [`JsonCodecSettingsBuilder`] pre-populated with this
263    /// instance's current values. Useful for protocol wrappers that
264    /// need to rebuild a codec with one setting overridden:
265    ///
266    /// ```
267    /// use aws_smithy_json::codec::JsonCodecSettings;
268    ///
269    /// let original = JsonCodecSettings::default();
270    /// let modified = original.to_builder()
271    ///     .default_namespace("com.example")
272    ///     .build();
273    /// ```
274    pub fn to_builder(&self) -> JsonCodecSettingsBuilder {
275        JsonCodecSettingsBuilder {
276            use_json_name: matches!(self.field_mapper, JsonFieldMapper::UseJsonName),
277            default_timestamp_format: self.default_timestamp_format,
278            max_depth: self.max_depth,
279            enforce_strictness: self.enforce_strictness,
280            validate_skipped_values: self.validate_skipped_values,
281            validate_skipped_string_encoding: self.validate_skipped_string_encoding,
282            allow_integral_float_numbers: self.allow_integral_float_numbers,
283            allow_leading_zeros: self.allow_leading_zeros,
284            allow_trailing_decimal_point: self.allow_trailing_decimal_point,
285            allow_empty_integer_part: self.allow_empty_integer_part,
286            strict_timestamp_formats: self.strict_timestamp_formats,
287            dispatch_null_members: self.dispatch_null_members,
288            protocol_id: self.protocol_id.clone(),
289            use_string_for_arbitrary_precision: self.use_string_for_arbitrary_precision,
290            normalize_arbitrary_precision: self.normalize_arbitrary_precision,
291            default_namespace: self.default_namespace.clone(),
292        }
293    }
294
295    /// Returns the encoded JSON object key (`"name":`) for a member schema.
296    #[inline]
297    pub(crate) fn member_key<'s>(&self, member: &'s Schema<'_>) -> Option<&'s str> {
298        self.field_mapper.member_key(member)
299    }
300
301    /// Resolves a JSON wire field name to a member schema.
302    pub(crate) fn field_to_member<'s>(
303        &self,
304        schema: &'s Schema<'s>,
305        field_name: &str,
306    ) -> Option<&'s Schema<'s>> {
307        self.field_mapper.field_to_member(schema, field_name)
308    }
309}
310
311impl Default for JsonCodecSettings {
312    fn default() -> Self {
313        Self {
314            field_mapper: JsonFieldMapper::UseJsonName,
315            default_timestamp_format: TimestampFormat::EpochSeconds,
316            max_depth: crate::codec::deserializer::MAX_DESERIALIZE_DEPTH,
317            enforce_strictness: false,
318            validate_skipped_values: true,
319            validate_skipped_string_encoding: false,
320            allow_integral_float_numbers: false,
321            allow_leading_zeros: false,
322            allow_trailing_decimal_point: false,
323            allow_empty_integer_part: false,
324            strict_timestamp_formats: false,
325            dispatch_null_members: false,
326            protocol_id: DEFAULT_JSON_CODEC_ID,
327            use_string_for_arbitrary_precision: false,
328            normalize_arbitrary_precision: false,
329            default_namespace: None,
330        }
331    }
332}
333
334/// Sentinel protocol id used by `JsonCodecSettings` when none is
335/// supplied — surfaces in `DocumentSettings` diagnostics. AWS
336/// protocols override this with their own ids.
337const DEFAULT_JSON_CODEC_ID: ShapeId<'static> = shape_id!("aws.smithy.json", "JsonCodec");
338
339/// Selects the timestamp [`Format`](aws_smithy_types::date_time::Format) used
340/// to parse a *string*-encoded timestamp, given a codec's configured default
341/// format.
342///
343/// A JSON string never encodes `epoch-seconds` (a number), so every default
344/// other than `http-date` resolves to the offset-aware `date-time` form — the
345/// common JSON string-timestamp encoding and a lenient superset of strict
346/// `date-time`. `http-date` is used as-is.
347///
348/// Shared by the untyped-document coercion path (`coerce_string_to_timestamp`)
349/// and the schema-based read path (`read_timestamp`) so the two string-
350/// timestamp paths cannot drift.
351pub(crate) fn string_timestamp_format(default: TimestampFormat) -> TimestampFormat {
352    match default {
353        TimestampFormat::HttpDate => TimestampFormat::HttpDate,
354        _ => TimestampFormat::DateTimeWithOffset,
355    }
356}
357
358/// Builder for [`JsonCodecSettings`].
359#[derive(Debug, Clone)]
360pub struct JsonCodecSettingsBuilder {
361    use_json_name: bool,
362    default_timestamp_format: TimestampFormat,
363    max_depth: u32,
364    strict_timestamp_formats: bool,
365    dispatch_null_members: bool,
366    enforce_strictness: bool,
367    validate_skipped_values: bool,
368    validate_skipped_string_encoding: bool,
369    allow_integral_float_numbers: bool,
370    allow_leading_zeros: bool,
371    allow_trailing_decimal_point: bool,
372    allow_empty_integer_part: bool,
373    protocol_id: ShapeId<'static>,
374    use_string_for_arbitrary_precision: bool,
375    normalize_arbitrary_precision: bool,
376    default_namespace: Option<String>,
377}
378
379impl Default for JsonCodecSettingsBuilder {
380    fn default() -> Self {
381        Self {
382            use_json_name: true,
383            default_timestamp_format: TimestampFormat::EpochSeconds,
384            max_depth: crate::codec::deserializer::MAX_DESERIALIZE_DEPTH,
385            enforce_strictness: false,
386            validate_skipped_values: true,
387            validate_skipped_string_encoding: false,
388            allow_integral_float_numbers: false,
389            allow_leading_zeros: false,
390            allow_trailing_decimal_point: false,
391            allow_empty_integer_part: false,
392            strict_timestamp_formats: false,
393            dispatch_null_members: false,
394            protocol_id: DEFAULT_JSON_CODEC_ID,
395            use_string_for_arbitrary_precision: false,
396            normalize_arbitrary_precision: false,
397            default_namespace: None,
398        }
399    }
400}
401
402impl JsonCodecSettingsBuilder {
403    /// Enforces JSON grammar and request document validity. Disabled by default.
404    pub fn enforce_strictness(mut self, value: bool) -> Self {
405        self.enforce_strictness = value;
406        self
407    }
408
409    /// Whether discarded string contents are validated. Enabled by default.
410    ///
411    /// When enabled, escape syntax is checked without decoding Unicode escapes.
412    /// `enforce_strictness` applies only to values read. When disabled, strings and keys inside
413    /// discarded objects are scanned without decoding or validating escapes.
414    /// Container syntax, number/literal grammar, truncation, and depth limits
415    /// are still checked. Values actually read are unaffected.
416    pub fn validate_skipped_values(mut self, value: bool) -> Self {
417        self.validate_skipped_values = value;
418        self
419    }
420
421    /// Checks discarded strings for UTF-8 and unescaped control characters without
422    /// decoding or validating their escape sequences. Disabled by default.
423    pub fn validate_skipped_string_encoding(mut self, value: bool) -> Self {
424        self.validate_skipped_string_encoding = value;
425        self
426    }
427
428    /// Accepts exactly integral decimal/exponent numbers for integer members.
429    /// Disabled by default; ordinary integer spellings retain exact parsing.
430    pub fn allow_integral_float_numbers(mut self, value: bool) -> Self {
431        self.allow_integral_float_numbers = value;
432        self
433    }
434
435    /// Permits leading zeros in JSON numbers even with `enforce_strictness(true)`.
436    /// Disabled by default. This matches the legacy JSON parser without relaxing
437    /// other grammar checks. Applies to read and skipped numbers alike.
438    pub fn allow_leading_zeros(mut self, value: bool) -> Self {
439        self.allow_leading_zeros = value;
440        self
441    }
442
443    /// Permits an empty fraction after a decimal point, such as `12.` or `12.e2`,
444    /// even with `enforce_strictness(true)`. Disabled by default. Integer members
445    /// additionally require `allow_integral_float_numbers(true)` and a whole value.
446    /// Applies to read and skipped numbers alike.
447    pub fn allow_trailing_decimal_point(mut self, value: bool) -> Self {
448        self.allow_trailing_decimal_point = value;
449        self
450    }
451
452    /// Permits negative fractions with no integer digits, such as `-.123` or
453    /// `-.123e2`, even with `enforce_strictness(true)`. Disabled by default.
454    /// Under strict number validation, `.123`, `-.`, and `-.e2` remain invalid.
455    /// Applies to read and skipped numbers alike.
456    pub fn allow_empty_integer_part(mut self, value: bool) -> Self {
457        self.allow_empty_integer_part = value;
458        self
459    }
460
461    /// Whether to use the `@jsonName` trait for member names.
462    pub fn use_json_name(mut self, value: bool) -> Self {
463        self.use_json_name = value;
464        self
465    }
466
467    /// Default timestamp format when not specified by `@timestampFormat` trait.
468    pub fn default_timestamp_format(mut self, value: TimestampFormat) -> Self {
469        self.default_timestamp_format = value;
470        self
471    }
472
473    /// Sets the maximum aggregate nesting depth the deserializer will accept
474    /// before returning an error. Defaults to 128.
475    pub fn max_depth(mut self, value: u32) -> Self {
476        self.max_depth = value;
477        self
478    }
479
480    /// Require timestamps to use exactly the wire form their resolved format
481    /// prescribes: a JSON number for `epoch-seconds`, an RFC 3339 string without
482    /// a UTC offset for `date-time`, an IMF-fixdate string for `http-date`.
483    /// Off by default; when enabled, any other form is rejected.
484    pub fn strict_timestamp_formats(mut self, value: bool) -> Self {
485        self.strict_timestamp_formats = value;
486        self
487    }
488
489    /// Pass null structure members to the consumer. Defaults to false, preserving existing
490    /// codec callers; schema server readers decide how each null affects their builders.
491    pub fn dispatch_null_members(mut self, value: bool) -> Self {
492        self.dispatch_null_members = value;
493        self
494    }
495
496    /// Sets the protocol id surfaced in `DocumentSettings`
497    /// diagnostics. Defaults to `aws.smithy.json#JsonCodec`. AWS
498    /// protocols (awsJson1_0, awsJson1_1, restJson1) supply their own
499    /// shape id so coercion errors point at the originating protocol.
500    pub fn protocol_id(mut self, value: ShapeId<'static>) -> Self {
501        self.protocol_id = value;
502        self
503    }
504
505    /// Configures whether `bigInteger` and `bigDecimal` shapes
506    /// serialize as JSON strings (when `true`) or as raw JSON numbers
507    /// (when `false`, the default).
508    ///
509    /// This is schema-driven: a `Document` carries no schema and has no
510    /// arbitrary-precision variant, so `write_document` is unaffected.
511    ///
512    /// Set to `true` for interop with receivers using `f64`-routed
513    /// JSON parsers that would otherwise lose precision on large
514    /// integers or decimals. The read path always accepts both forms.
515    pub fn use_string_for_arbitrary_precision(mut self, value: bool) -> Self {
516        self.use_string_for_arbitrary_precision = value;
517        self
518    }
519
520    /// Rewrites arbitrary-precision text that is a decimal but not a JSON number (`+007`, `.5`,
521    /// `1.`) to the equivalent JSON number instead of rejecting it. Defaults to `false`, so
522    /// existing callers keep rejecting it; AWS servers set it to `true`, matching their lenient
523    /// handling of such numbers on the read path (`allow_leading_zeros`,
524    /// `allow_trailing_decimal_point`).
525    pub fn normalize_arbitrary_precision(mut self, value: bool) -> Self {
526        self.normalize_arbitrary_precision = value;
527        self
528    }
529
530    /// Sets the default Smithy namespace used to resolve relative shape
531    /// IDs in JSON `__type` discriminator fields. When set, a relative
532    /// `__type: "Capacity"` is lifted to `<namespace>#Capacity` on the
533    /// resulting [`DiscriminatedDocument`](aws_smithy_types::DiscriminatedDocument). Absolute `__type` values
534    /// (already containing `#`) are taken as-is regardless.
535    ///
536    /// Defaults to `None` — relative names are not lifted.
537    pub fn default_namespace(mut self, namespace: impl Into<String>) -> Self {
538        self.default_namespace = Some(namespace.into());
539        self
540    }
541
542    /// Builds the settings.
543    pub fn build(self) -> JsonCodecSettings {
544        let field_mapper = if self.use_json_name {
545            JsonFieldMapper::UseJsonName
546        } else {
547            JsonFieldMapper::UseMemberName
548        };
549        JsonCodecSettings {
550            field_mapper,
551            default_timestamp_format: self.default_timestamp_format,
552            max_depth: self.max_depth,
553            enforce_strictness: self.enforce_strictness,
554            validate_skipped_values: self.validate_skipped_values,
555            validate_skipped_string_encoding: self.validate_skipped_string_encoding,
556            allow_integral_float_numbers: self.allow_integral_float_numbers,
557            allow_leading_zeros: self.allow_leading_zeros,
558            allow_trailing_decimal_point: self.allow_trailing_decimal_point,
559            allow_empty_integer_part: self.allow_empty_integer_part,
560            strict_timestamp_formats: self.strict_timestamp_formats,
561            dispatch_null_members: self.dispatch_null_members,
562            protocol_id: self.protocol_id,
563            use_string_for_arbitrary_precision: self.use_string_for_arbitrary_precision,
564            normalize_arbitrary_precision: self.normalize_arbitrary_precision,
565            default_namespace: self.default_namespace,
566        }
567    }
568}
569
570impl DocumentSettings for JsonCodecSettings {
571    fn protocol_id(&self) -> &str {
572        // The internal field is `ShapeId<'static>` (preserved for
573        // ergonomic construction via `shape_id!`); the trait surface
574        // returns the FQN string per [`DocumentSettings`].
575        self.protocol_id.as_str()
576    }
577
578    /// Decodes a base64 string into a blob.
579    ///
580    /// JSON has no native blob type; the
581    /// [Smithy spec](https://smithy.io/2.0/spec/simple-types.html#blob)
582    /// requires blobs to be transmitted as base64-encoded strings on
583    /// the JSON wire. This method consumes those strings on the
584    /// deserialization side; the corresponding encode happens in
585    /// `JsonSerializer::write_document`.
586    fn coerce_string_to_blob(&self, s: &str) -> Result<Vec<u8>, DocumentError> {
587        aws_smithy_types::base64::decode(s)
588            .map_err(|e| DocumentError::invalid_input(format!("base64 decode failed: {e}")))
589    }
590
591    /// Parses a string-formatted timestamp using the codec's default
592    /// timestamp format ([`JsonCodecSettings::default_timestamp_format`]).
593    ///
594    /// Per the SEP, untyped JSON documents have no schema-level
595    /// `@timestampFormat` trait to consult, so the codec's default
596    /// format is the only signal. AWS JSON / restJson1 default to
597    /// `epoch-seconds` — but a string-typed JSON value cannot be
598    /// epoch-seconds (which is a number). For this case we attempt
599    /// `date-time` parsing as a fallback so common ISO-8601 strings
600    /// still coerce.
601    fn coerce_string_to_timestamp(&self, s: &str) -> Result<DateTime, DocumentError> {
602        // A JSON string can't encode the number-typed `epoch-seconds`, so the
603        // parse format is resolved via the shared `string_timestamp_format`
604        // helper (also used by the schema-based read path).
605        let format = string_timestamp_format(self.default_timestamp_format);
606        DateTime::from_str(s, format)
607            .map_err(|e| DocumentError::invalid_input(format!("timestamp parse failed: {e}")))
608    }
609
610    /// Coerces a number to a timestamp interpreted as
611    /// `epoch-seconds`.
612    ///
613    /// Number-typed JSON values are only valid timestamps when the
614    /// codec's default format is `epoch-seconds`. Per the SEP this is
615    /// the AWS JSON / restJson1 default. If the codec has been
616    /// configured with a non-numeric default format
617    /// (`date-time` / `http-date`), this method returns an
618    /// `UnsupportedOperation` error — a number value isn't valid for
619    /// those formats.
620    fn coerce_number_to_timestamp(&self, n: &Number) -> Result<DateTime, DocumentError> {
621        if !matches!(self.default_timestamp_format, TimestampFormat::EpochSeconds) {
622            return Err(DocumentError::unsupported(format!(
623                "JSON codec configured with timestamp format {:?}; \
624                 number-to-timestamp coercion only valid for epoch-seconds",
625                self.default_timestamp_format
626            )));
627        }
628        Ok(match *n {
629            Number::PosInt(u) => {
630                if u > i64::MAX as u64 {
631                    return Err(DocumentError::invalid_input(format!(
632                        "epoch-seconds value {u} overflows i64; cannot construct DateTime"
633                    )));
634                }
635                DateTime::from_secs(u as i64)
636            }
637            Number::NegInt(i) => DateTime::from_secs(i),
638            Number::Float(f) => {
639                if !f.is_finite() {
640                    return Err(DocumentError::invalid_input(format!(
641                        "non-finite epoch-seconds value {f}; cannot construct DateTime"
642                    )));
643                }
644                DateTime::from_secs_f64(f)
645            }
646        })
647    }
648}
649
650/// JSON codec for schema-based serialization and deserialization.
651///
652/// # Examples
653///
654/// ```
655/// use aws_smithy_json::codec::{JsonCodec, JsonCodecSettings};
656/// use aws_smithy_schema::codec::Codec;
657///
658/// // Create codec with default settings (REST JSON style)
659/// let codec = JsonCodec::new(JsonCodecSettings::default());
660///
661/// // Create codec for AWS JSON RPC (no jsonName, epoch-seconds timestamps)
662/// let codec = JsonCodec::new(
663///     JsonCodecSettings::builder()
664///         .use_json_name(false)
665///         .build()
666/// );
667/// ```
668#[derive(Debug)]
669pub struct JsonCodec {
670    settings: Arc<JsonCodecSettings>,
671}
672
673impl JsonCodec {
674    /// Creates a new JSON codec with the given settings.
675    pub fn new(settings: JsonCodecSettings) -> Self {
676        Self {
677            settings: Arc::new(settings),
678        }
679    }
680
681    /// Returns the codec settings.
682    pub fn settings(&self) -> &JsonCodecSettings {
683        &self.settings
684    }
685}
686
687impl Default for JsonCodec {
688    fn default() -> Self {
689        Self::new(JsonCodecSettings::default())
690    }
691}
692
693impl Codec for JsonCodec {
694    type Serializer = JsonSerializer;
695    type Deserializer<'a> = JsonDeserializer<'a>;
696
697    fn create_serializer(&self) -> Self::Serializer {
698        JsonSerializer::new(self.settings.clone())
699    }
700
701    fn create_deserializer<'a>(&self, input: &'a [u8]) -> Self::Deserializer<'a> {
702        JsonDeserializer::new(input, self.settings.clone())
703    }
704}
705
706#[cfg(test)]
707mod tests {
708    use super::*;
709
710    #[test]
711    fn test_default_settings() {
712        let settings = JsonCodecSettings::default();
713        assert_eq!(
714            settings.default_timestamp_format(),
715            TimestampFormat::EpochSeconds
716        );
717    }
718
719    #[test]
720    fn test_builder() {
721        let settings = JsonCodecSettings::builder()
722            .use_json_name(false)
723            .default_timestamp_format(TimestampFormat::DateTime)
724            .build();
725        assert_eq!(
726            settings.default_timestamp_format(),
727            TimestampFormat::DateTime
728        );
729    }
730
731    #[test]
732    fn test_codec_creation() {
733        let codec = JsonCodec::default();
734        let _serializer = codec.create_serializer();
735        let _deserializer = codec.create_deserializer(b"{}");
736    }
737
738    #[test]
739    fn document_settings_protocol_id_default_is_codec_sentinel() {
740        let settings = JsonCodecSettings::default();
741        assert_eq!(
742            DocumentSettings::protocol_id(&settings),
743            "aws.smithy.json#JsonCodec"
744        );
745    }
746
747    #[test]
748    fn document_settings_protocol_id_can_be_overridden() {
749        let settings = JsonCodecSettings::builder()
750            .protocol_id(shape_id!("aws.protocols", "restJson1"))
751            .build();
752        assert_eq!(
753            DocumentSettings::protocol_id(&settings),
754            "aws.protocols#restJson1"
755        );
756    }
757
758    #[test]
759    fn document_settings_coerce_string_to_blob_decodes_base64() {
760        let settings = JsonCodecSettings::default();
761        // base64 of "hello"
762        let bytes = settings.coerce_string_to_blob("aGVsbG8=").unwrap();
763        assert_eq!(bytes, b"hello");
764    }
765
766    #[test]
767    fn document_settings_coerce_string_to_blob_returns_error_on_invalid() {
768        let settings = JsonCodecSettings::default();
769        let err = settings
770            .coerce_string_to_blob("not!valid!base64!")
771            .unwrap_err();
772        match err {
773            DocumentError::InvalidInput { .. } => {}
774            other => panic!("expected DocumentError::InvalidInput, got {other:?}"),
775        }
776    }
777
778    #[test]
779    fn document_settings_coerce_string_to_timestamp_with_date_time_default() {
780        let settings = JsonCodecSettings::builder()
781            .default_timestamp_format(TimestampFormat::DateTime)
782            .build();
783        let dt = settings
784            .coerce_string_to_timestamp("1970-01-01T00:00:00Z")
785            .unwrap();
786        assert_eq!(dt.secs(), 0);
787    }
788
789    #[test]
790    fn document_settings_coerce_string_to_timestamp_falls_back_to_date_time_for_epoch_default() {
791        // Default format is EpochSeconds (numeric); a string can't
792        // satisfy that, so the impl falls back to date-time parsing.
793        let settings = JsonCodecSettings::default();
794        let dt = settings
795            .coerce_string_to_timestamp("1970-01-01T00:00:00Z")
796            .unwrap();
797        assert_eq!(dt.secs(), 0);
798    }
799
800    #[test]
801    fn document_settings_coerce_string_to_timestamp_error() {
802        let settings = JsonCodecSettings::default();
803        let err = settings
804            .coerce_string_to_timestamp("not a timestamp")
805            .unwrap_err();
806        match err {
807            DocumentError::InvalidInput { .. } => {}
808            other => panic!("expected DocumentError::InvalidInput, got {other:?}"),
809        }
810    }
811
812    #[test]
813    fn document_settings_coerce_number_to_timestamp_epoch_seconds() {
814        let settings = JsonCodecSettings::default();
815        // PosInt
816        let dt = settings
817            .coerce_number_to_timestamp(&Number::PosInt(1_700_000_000))
818            .unwrap();
819        assert_eq!(dt.secs(), 1_700_000_000);
820        // NegInt (pre-epoch)
821        let dt = settings
822            .coerce_number_to_timestamp(&Number::NegInt(-100))
823            .unwrap();
824        assert_eq!(dt.secs(), -100);
825        // Float (fractional seconds)
826        let dt = settings
827            .coerce_number_to_timestamp(&Number::Float(1.5))
828            .unwrap();
829        assert_eq!(dt.secs(), 1);
830        assert_eq!(dt.subsec_nanos(), 500_000_000);
831    }
832
833    #[test]
834    fn document_settings_coerce_number_to_timestamp_unsupported_for_string_format() {
835        let settings = JsonCodecSettings::builder()
836            .default_timestamp_format(TimestampFormat::DateTime)
837            .build();
838        let err = settings
839            .coerce_number_to_timestamp(&Number::PosInt(0))
840            .unwrap_err();
841        match err {
842            DocumentError::UnsupportedOperation { .. } => {}
843            other => panic!("expected DocumentError::UnsupportedOperation, got {other:?}"),
844        }
845    }
846
847    #[test]
848    fn document_settings_coerce_number_to_timestamp_rejects_non_finite_float() {
849        let settings = JsonCodecSettings::default();
850        let err = settings
851            .coerce_number_to_timestamp(&Number::Float(f64::NAN))
852            .unwrap_err();
853        match err {
854            DocumentError::InvalidInput { .. } => {}
855            other => panic!("expected DocumentError::InvalidInput, got {other:?}"),
856        }
857    }
858
859    #[test]
860    fn to_builder_round_trips_every_setting() {
861        // Regression guard: `to_builder` names each field explicitly, so a
862        // setting added later is silently reset to its default here while still
863        // compiling. That is how a codec rebuilt by a protocol wrapper (see
864        // `protocol::codec_with_bag_namespace`, which overrides only
865        // `default_namespace`) can lose strictness. Every setting below is set
866        // to the OPPOSITE of its default so a dropped field fails this test.
867        let original = JsonCodecSettings::builder()
868            .use_json_name(false)
869            .default_timestamp_format(TimestampFormat::HttpDate)
870            .max_depth(7)
871            .protocol_id(shape_id!("aws.protocols", "restJson1"))
872            .use_string_for_arbitrary_precision(true)
873            .default_namespace("com.example")
874            .enforce_strictness(true)
875            .validate_skipped_values(false)
876            .allow_integral_float_numbers(true)
877            .allow_leading_zeros(true)
878            .allow_trailing_decimal_point(true)
879            .allow_empty_integer_part(true)
880            .strict_timestamp_formats(true)
881            .dispatch_null_members(true)
882            .build();
883
884        let round_tripped = original.to_builder().build();
885
886        assert!(matches!(
887            round_tripped.field_mapper,
888            JsonFieldMapper::UseMemberName
889        ));
890        assert_eq!(
891            round_tripped.default_timestamp_format(),
892            TimestampFormat::HttpDate
893        );
894        assert_eq!(round_tripped.max_depth(), 7);
895        assert_eq!(
896            DocumentSettings::protocol_id(&round_tripped),
897            "aws.protocols#restJson1"
898        );
899        assert!(round_tripped.use_string_for_arbitrary_precision());
900        assert_eq!(round_tripped.default_namespace(), Some("com.example"));
901        assert!(round_tripped.enforce_strictness);
902        assert!(!round_tripped.validate_skipped_values);
903        assert!(round_tripped.allow_integral_float_numbers);
904        assert!(round_tripped.allow_leading_zeros);
905        assert!(round_tripped.allow_trailing_decimal_point);
906        assert!(round_tripped.allow_empty_integer_part);
907        assert!(round_tripped.strict_timestamp_formats());
908        assert!(round_tripped.dispatch_null_members());
909    }
910}