Skip to main content

aws_smithy_json/codec/
serializer.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! JSON serializer implementation.
7
8use aws_smithy_schema::serde::{SerdeError, SerializableStruct, ShapeSerializer};
9use aws_smithy_schema::Schema;
10use aws_smithy_types::date_time::Format as TimestampFormat;
11use aws_smithy_types::primitive::Encoder;
12use aws_smithy_types::{BigDecimal, BigInteger, DateTime, DiscriminatedDocument, Document};
13
14use crate::codec::JsonCodecSettings;
15
16use std::sync::Arc;
17
18/// `true` iff `s` is non-empty and consists entirely of ASCII digits.
19fn is_ascii_digits(s: &str) -> bool {
20    !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
21}
22
23/// `true` iff `text` matches the RFC 8259 JSON number grammar:
24/// `'-'? ('0' | [1-9][0-9]*) ('.' [0-9]+)? ([eE] [+-]? [0-9]+)?`.
25///
26/// [`aws_smithy_types::BigInteger`] and [`aws_smithy_types::BigDecimal`]
27/// are string wrappers whose `FromStr` implementations validate their own
28/// supported numeric grammars. `BigInteger` accepts `'-'? DIGIT+`, while
29/// `BigDecimal` also accepts forms such as `"+123"`, `".5"`, and `"1."`.
30/// Both deliberately accept leading zeros for compatibility with previously
31/// released versions.
32///
33/// Several of those valid arbitrary-precision representations are not valid
34/// RFC 8259 JSON numbers. The raw-number path therefore rejects a leading `+`,
35/// an omitted integer or fractional digit run, and leading zeros rather than
36/// producing JSON that this codec cannot read back, or, with
37/// `normalize_arbitrary_precision`, rewrites them into the equivalent JSON number.
38///
39/// This remains a complete grammar check, rather than assuming every value was
40/// constructed through `FromStr`, so malformed internal values cannot reach the
41/// output buffer either.
42fn is_json_number(text: &str) -> bool {
43    let rest = text.strip_prefix('-').unwrap_or(text);
44
45    let (mantissa, exponent) = match rest.split_once(['e', 'E']) {
46        Some((mantissa, exponent)) => (mantissa, Some(exponent)),
47        None => (rest, None),
48    };
49
50    let (int_part, frac_part) = match mantissa.split_once('.') {
51        Some((int_part, frac_part)) => (int_part, Some(frac_part)),
52        None => (mantissa, None),
53    };
54
55    // int = '0' / ( digit1-9 *DIGIT ) — no leading zeros, no empty run.
56    let int_ok = match int_part.as_bytes() {
57        [b'0'] => true,
58        [b'1'..=b'9', tail @ ..] => tail.iter().all(u8::is_ascii_digit),
59        _ => false,
60    };
61
62    // frac = '.' 1*DIGIT — the digit run may not be empty.
63    let frac_ok = match frac_part {
64        None => true,
65        Some(frac) => is_ascii_digits(frac),
66    };
67
68    // exp = ('e' / 'E') [ '-' / '+' ] 1*DIGIT
69    let exponent_ok = match exponent {
70        None => true,
71        Some(exponent) => is_ascii_digits(exponent.strip_prefix(['+', '-']).unwrap_or(exponent)),
72    };
73
74    int_ok && frac_ok && exponent_ok
75}
76
77/// Rewrites arbitrary-precision text that is a valid decimal but not a valid RFC 8259 JSON number
78/// (a leading `+`, leading zeros, an omitted integer or fraction digit run) into the equivalent
79/// JSON number, e.g. `+000987654321` becomes `987654321` and `.5` becomes `0.5`.
80///
81/// The value is unchanged; only its spelling is. Returns `None` if `text` is not a decimal at all.
82fn to_json_number(text: &str) -> Option<String> {
83    let (coefficient, exponent) = match text.split_once(['e', 'E']) {
84        Some((coefficient, exponent)) => (coefficient, Some(exponent)),
85        None => (text, None),
86    };
87    let (negative, unsigned) = match coefficient.strip_prefix('-') {
88        Some(rest) => (true, rest),
89        None => (false, coefficient.strip_prefix('+').unwrap_or(coefficient)),
90    };
91    let (integer, fraction) = unsigned.split_once('.').unwrap_or((unsigned, ""));
92    if (integer.is_empty() && fraction.is_empty())
93        || !integer
94            .bytes()
95            .chain(fraction.bytes())
96            .all(|b| b.is_ascii_digit())
97    {
98        return None;
99    }
100    let integer = integer.trim_start_matches('0');
101    let mut out = String::with_capacity(text.len());
102    if negative {
103        out.push('-');
104    }
105    out.push_str(if integer.is_empty() { "0" } else { integer });
106    if !fraction.is_empty() {
107        out.push('.');
108        out.push_str(fraction);
109    }
110    if let Some(exponent) = exponent {
111        out.push('e');
112        out.push_str(exponent);
113    }
114    is_json_number(&out).then_some(out)
115}
116
117/// JSON serializer that implements the ShapeSerializer trait.
118pub struct JsonSerializer {
119    output: String,
120    settings: Arc<JsonCodecSettings>,
121    // Tracks whether a comma is needed before the next value in the current container.
122    needs_comma: bool,
123    // When true, the next write_string is a map key — emit "key": instead of ,"key"
124    expecting_map_key: bool,
125    // Nesting depth of write_map calls. When >0, prefix() restores expecting_map_key
126    // after each value write so the next write_string is treated as a key.
127    map_depth: usize,
128    // True when the current container is a struct/union (so a member schema's
129    // member_name should be emitted as a JSON field name). False when the
130    // current container is a list or map (so a member schema's member_name —
131    // typically the position name "member"/"value" — must NOT leak into the
132    // output as a spurious field key). Saved and restored across nested
133    // write_struct / write_list / write_map calls.
134    in_struct_context: bool,
135}
136
137impl JsonSerializer {
138    /// Creates a new JSON serializer with the given settings.
139    pub(crate) fn new(settings: Arc<JsonCodecSettings>) -> Self {
140        Self {
141            output: String::new(),
142            settings,
143            needs_comma: false,
144            expecting_map_key: false,
145            map_depth: 0,
146            // Top-level call sites typically pass a top-level (non-member)
147            // schema, so this default value rarely matters. It only affects
148            // behavior if the very first write_* call passes a member
149            // schema — in which case treating the first frame as struct
150            // context preserves backward compatibility with any caller
151            // relying on `,"name":` emission at the top level.
152            in_struct_context: true,
153        }
154    }
155
156    /// Finalizes the serialization and returns the output as bytes.
157    pub fn finish(self) -> Vec<u8> {
158        self.output.into_bytes()
159    }
160
161    /// Handles comma separators and member names before writing a value.
162    /// When inside a map (map_depth > 0), restores expecting_map_key after
163    /// the value so the next write_string is treated as a map key.
164    fn prefix(&mut self, schema: &Schema<'_>) {
165        if self.needs_comma {
166            self.output.push(',');
167        }
168        // Only emit a JSON field name when we are inside a struct/union
169        // body. For list elements and map values the schema may still be a
170        // member schema (e.g. an inner aggregate's `_VALUE` carrying
171        // `@xmlName` for the XML codec), but that member's name is a
172        // *position label* — not a JSON field key.
173        if self.in_struct_context {
174            if let Some(key) = self.settings.member_key(schema) {
175                self.output.push_str(key);
176            }
177        }
178        self.needs_comma = true;
179        // Inside a map, after writing a value the next write_string should be a key.
180        // This is safe because write_string checks expecting_map_key *before* calling
181        // prefix(), so this only affects the *next* write_string call.
182        if self.map_depth > 0 {
183            self.expecting_map_key = true;
184        }
185    }
186
187    /// Gets the timestamp format to use, respecting @timestampFormat trait.
188    fn get_timestamp_format(&self, schema: &Schema<'_>) -> Result<TimestampFormat, SerdeError> {
189        if let Some(ts_trait) = schema.timestamp_format() {
190            return Ok(match ts_trait.format() {
191                aws_smithy_schema::traits::TimestampFormat::EpochSeconds => {
192                    TimestampFormat::EpochSeconds
193                }
194                aws_smithy_schema::traits::TimestampFormat::HttpDate => TimestampFormat::HttpDate,
195                aws_smithy_schema::traits::TimestampFormat::DateTime => TimestampFormat::DateTime,
196                other => {
197                    return Err(SerdeError::unsupported(format!(
198                        "unsupported timestamp format {other:?}"
199                    )))
200                }
201            });
202        }
203        Ok(self.settings.default_timestamp_format)
204    }
205
206    /// Writes an arbitrary-precision numeric string.
207    ///
208    /// By default the decimal text is emitted verbatim as a raw JSON
209    /// number, so a receiver with an arbitrary-precision parser recovers
210    /// the exact value with no `f64` round-trip. Emitting verbatim means
211    /// the text itself has to be a JSON number, which
212    /// [`aws_smithy_types::BigInteger`] / [`aws_smithy_types::BigDecimal`]
213    /// do not guarantee: their `FromStr` is structurally strict but
214    /// deliberately accepts leading zeros (`"00123"`), which RFC 8259
215    /// forbids (see [`is_json_number`]). The raw path therefore reports such
216    /// text as [`SerdeError::InvalidInput`] rather than emitting malformed
217    /// JSON, unless [`JsonCodecSettings::normalize_arbitrary_precision`] is
218    /// set, in which case it is rewritten to the equivalent JSON number
219    /// (`+007` becomes `7`; see [`to_json_number`]).
220    ///
221    /// When [`JsonCodecSettings::use_string_for_arbitrary_precision`] is
222    /// set, the value is emitted as a JSON string instead — for interop
223    /// with receivers that route every JSON number through `f64` and
224    /// would otherwise lose precision on values that overflow it. That
225    /// path needs no grammar check: any text at all is representable
226    /// once escaped, so it cannot produce malformed JSON.
227    ///
228    /// This is driven by the schema: only `bigInteger` / `bigDecimal`
229    /// shapes reach it. A `Document` carries no schema and has no
230    /// arbitrary-precision variant, so `write_document` never takes this
231    /// path.
232    ///
233    /// Nothing is appended to the output when validation fails, so a
234    /// rejected value leaves no truncated fragment behind.
235    fn write_arbitrary_precision(
236        &mut self,
237        schema: &Schema<'_>,
238        text: &str,
239    ) -> Result<(), SerdeError> {
240        if self.settings.use_string_for_arbitrary_precision() {
241            self.prefix(schema);
242            crate::serialize::JsonValueWriter::new(&mut self.output).string(text);
243            return Ok(());
244        }
245        let normalized;
246        let text = if is_json_number(text) {
247            text
248        } else {
249            normalized = self
250                .settings
251                .normalize_arbitrary_precision()
252                .then(|| to_json_number(text))
253                .flatten()
254                .ok_or_else(|| {
255                    SerdeError::invalid_input(format!(
256                        "arbitrary-precision value {text:?} is not a JSON number, so it cannot be \
257                         emitted as a raw JSON number; enable \
258                         `JsonCodecSettings::use_string_for_arbitrary_precision` to send it as a \
259                         JSON string, or `normalize_arbitrary_precision` to rewrite it"
260                    ))
261                })?;
262            &normalized
263        };
264        self.prefix(schema);
265        self.output.push_str(text);
266        Ok(())
267    }
268
269    fn write_json_value(&mut self, doc: &Document) -> Result<(), SerdeError> {
270        use crate::escape::escape_string;
271        use crate::serialize::JsonValueWriter;
272
273        // Walk the [`aws_smithy_types::Document`] directly.
274        //
275        // Per the SEP § Document Types rule 8, the document is
276        // protocol-agnostic on the serialization side: it carries no
277        // per-member format trait (the schema is the carrier of those
278        // traits, and a `Document` value erases the schema).
279        //
280        // `Document` has exactly the six JSON-shaped variants, so every
281        // node maps one-to-one onto a JSON value. Smithy types with no
282        // native JSON form — blob, timestamp, bigInteger, bigDecimal —
283        // already ride their legacy representation inside those variants
284        // (base64 string, epoch-seconds number, numeric text), written
285        // by the schema-driven `write_blob` / `write_timestamp` /
286        // `write_big_*` paths which do inspect `@timestampFormat` and
287        // the codec settings.
288        //
289        // [`Document`] has no discriminator slot. Discriminated
290        // documents (with a `__type` field) flow through
291        // [`Self::write_discriminated_document`] which emits the
292        // wrapper's discriminator before delegating here for the
293        // payload.
294        match doc {
295            Document::Null => self.output.push_str("null"),
296            Document::Bool(b) => self.output.push_str(if *b { "true" } else { "false" }),
297            Document::Number(n) => {
298                let writer = JsonValueWriter::new(&mut self.output);
299                writer.number(*n);
300            }
301            Document::String(s) => {
302                self.output.push('"');
303                self.output.push_str(&escape_string(s));
304                self.output.push('"');
305            }
306            Document::Array(items) => {
307                self.output.push('[');
308                let mut first = true;
309                for item in items {
310                    if !first {
311                        self.output.push(',');
312                    }
313                    first = false;
314                    self.write_json_value(item)?;
315                }
316                self.output.push(']');
317            }
318            Document::Object(entries) => {
319                self.output.push('{');
320                let mut first = true;
321                for (key, value) in entries {
322                    if !first {
323                        self.output.push(',');
324                    }
325                    first = false;
326                    self.output.push('"');
327                    self.output.push_str(&escape_string(key));
328                    self.output.push_str("\":");
329                    self.write_json_value(value)?;
330                }
331                self.output.push('}');
332            } // `Document` is not `#[non_exhaustive]`, so this match is
333              // exhaustive today and adding a variant is a breaking change
334              // to `aws-smithy-types` that will fail this match to compile.
335              // That is the intent: a new variant needs a deliberate
336              // decision here rather than a silent no-op.
337        }
338        Ok(())
339    }
340
341    /// Serialize a [`DiscriminatedDocument`] to the codec's output.
342    ///
343    /// When the wrapper carries a discriminator and the inner Document
344    /// is a [`Document::Object`], the discriminator is emitted as a
345    /// `__type` field at the start of the JSON object per the SEP
346    /// § Typed Document Serialization (always serialized as an
347    /// absolute shape ID, the FQN form `namespace#name`).
348    ///
349    /// For non-object inner documents, or when no discriminator is
350    /// attached, this delegates to the plain JSON serialization path.
351    pub fn write_discriminated_document(
352        &mut self,
353        schema: &Schema<'_>,
354        value: &DiscriminatedDocument,
355    ) -> Result<(), SerdeError> {
356        use crate::escape::escape_string;
357
358        self.prefix(schema);
359        match (value.discriminator(), value.document()) {
360            (Some(id), Document::Object(entries)) => {
361                // SEP § Typed Document Serialization: a serialized
362                // discriminator MUST be an absolute shape id so the
363                // document stays context-free for downstream readers.
364                // `DiscriminatedDocument::with_discriminator` enforces
365                // this at construction; this is a defense-in-depth
366                // check at the wire boundary for any document produced
367                // internally.
368                debug_assert!(
369                    id.contains('#'),
370                    "discriminator `{id}` must be an absolute shape id (namespace#name) when serialized"
371                );
372                self.output.push('{');
373                self.output.push_str("\"__type\":\"");
374                self.output.push_str(&escape_string(id));
375                self.output.push('"');
376                for (key, val) in entries {
377                    self.output.push(',');
378                    self.output.push('"');
379                    self.output.push_str(&escape_string(key));
380                    self.output.push_str("\":");
381                    self.write_json_value(val)?;
382                }
383                self.output.push('}');
384                Ok(())
385            }
386            // No discriminator, or discriminator on a non-object payload
387            // — emit the inner document plainly.
388            (_, doc) => self.write_json_value(doc),
389        }
390    }
391}
392
393impl aws_smithy_schema::codec::FinishSerializer for JsonSerializer {
394    fn finish(self) -> Vec<u8> {
395        self.output.into_bytes()
396    }
397}
398
399impl ShapeSerializer for JsonSerializer {
400    fn write_struct(
401        &mut self,
402        schema: &Schema<'_>,
403        value: &dyn SerializableStruct,
404    ) -> Result<(), SerdeError> {
405        self.prefix(schema);
406        self.output.push('{');
407        let saved_comma = self.needs_comma;
408        let saved_depth = self.map_depth;
409        let saved_map_key = self.expecting_map_key;
410        let saved_struct_context = self.in_struct_context;
411        self.needs_comma = false;
412        // Reset map state so struct members don't trigger map-key logic.
413        // Restored after the struct body so an enclosing map resumes correctly.
414        self.map_depth = 0;
415        self.expecting_map_key = false;
416        // Mark the body as struct context so member-schema field names emit.
417        self.in_struct_context = true;
418        value.serialize_members(self)?;
419        self.output.push('}');
420        self.needs_comma = saved_comma;
421        self.map_depth = saved_depth;
422        self.expecting_map_key = saved_map_key;
423        self.in_struct_context = saved_struct_context;
424        Ok(())
425    }
426
427    fn write_list(
428        &mut self,
429        schema: &Schema<'_>,
430        write_elements: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
431    ) -> Result<(), SerdeError> {
432        self.prefix(schema);
433        self.output.push('[');
434        let saved = self.needs_comma;
435        let saved_depth = self.map_depth;
436        let saved_map_key = self.expecting_map_key;
437        let saved_struct_context = self.in_struct_context;
438        self.needs_comma = false;
439        // Reset map state so list elements don't trigger map-key logic in prefix().
440        self.map_depth = 0;
441        self.expecting_map_key = false;
442        // Mark the body as non-struct so a nested member schema's
443        // member_name (e.g. "member") doesn't leak as a JSON field key.
444        self.in_struct_context = false;
445        write_elements(self)?;
446        self.output.push(']');
447        self.needs_comma = saved;
448        self.map_depth = saved_depth;
449        self.expecting_map_key = saved_map_key;
450        self.in_struct_context = saved_struct_context;
451        Ok(())
452    }
453
454    fn write_map(
455        &mut self,
456        schema: &Schema<'_>,
457        write_entries: &dyn Fn(&mut dyn ShapeSerializer) -> Result<(), SerdeError>,
458    ) -> Result<(), SerdeError> {
459        self.prefix(schema);
460        self.output.push('{');
461        let saved_comma = self.needs_comma;
462        let saved_map_key = self.expecting_map_key;
463        let saved_depth = self.map_depth;
464        let saved_struct_context = self.in_struct_context;
465        self.needs_comma = false;
466        self.expecting_map_key = true;
467        // Increment depth so prefix() knows to restore expecting_map_key after
468        // each value write. write_string checks expecting_map_key *before* calling
469        // prefix(), so the flag only affects the *next* write_string (the next key).
470        self.map_depth += 1;
471        // Mark the body as non-struct so an inner aggregate's value-schema
472        // (a member schema with member_name "value") doesn't emit
473        // `"value":` before the actual value.
474        self.in_struct_context = false;
475        write_entries(self)?;
476        self.map_depth = saved_depth;
477        self.output.push('}');
478        self.needs_comma = saved_comma;
479        self.expecting_map_key = saved_map_key;
480        self.in_struct_context = saved_struct_context;
481        Ok(())
482    }
483
484    fn write_boolean(&mut self, schema: &Schema<'_>, value: bool) -> Result<(), SerdeError> {
485        self.prefix(schema);
486        self.output.push_str(if value { "true" } else { "false" });
487        Ok(())
488    }
489
490    fn write_byte(&mut self, schema: &Schema<'_>, value: i8) -> Result<(), SerdeError> {
491        use std::fmt::Write;
492        self.prefix(schema);
493        write!(&mut self.output, "{}", value).map_err(|e| SerdeError::write_failed(e.to_string()))
494    }
495
496    fn write_short(&mut self, schema: &Schema<'_>, value: i16) -> Result<(), SerdeError> {
497        use std::fmt::Write;
498        self.prefix(schema);
499        write!(&mut self.output, "{}", value).map_err(|e| SerdeError::write_failed(e.to_string()))
500    }
501
502    fn write_integer(&mut self, schema: &Schema<'_>, value: i32) -> Result<(), SerdeError> {
503        use std::fmt::Write;
504        self.prefix(schema);
505        write!(&mut self.output, "{}", value).map_err(|e| SerdeError::write_failed(e.to_string()))
506    }
507
508    fn write_long(&mut self, schema: &Schema<'_>, value: i64) -> Result<(), SerdeError> {
509        use std::fmt::Write;
510        self.prefix(schema);
511        write!(&mut self.output, "{}", value).map_err(|e| SerdeError::write_failed(e.to_string()))
512    }
513
514    fn write_float(&mut self, schema: &Schema<'_>, value: f32) -> Result<(), SerdeError> {
515        self.prefix(schema);
516        let mut encoder = Encoder::from(value);
517        if value.is_finite() {
518            self.output.push_str(encoder.encode());
519        } else {
520            self.output.push('"');
521            self.output.push_str(encoder.encode());
522            self.output.push('"');
523        }
524        Ok(())
525    }
526
527    fn write_double(&mut self, schema: &Schema<'_>, value: f64) -> Result<(), SerdeError> {
528        self.prefix(schema);
529        let mut encoder = Encoder::from(value);
530        if value.is_finite() {
531            self.output.push_str(encoder.encode());
532        } else {
533            self.output.push('"');
534            self.output.push_str(encoder.encode());
535            self.output.push('"');
536        }
537        Ok(())
538    }
539
540    fn write_big_integer(
541        &mut self,
542        schema: &Schema<'_>,
543        value: &BigInteger,
544    ) -> Result<(), SerdeError> {
545        self.write_arbitrary_precision(schema, value.as_ref())
546    }
547
548    fn write_big_decimal(
549        &mut self,
550        schema: &Schema<'_>,
551        value: &BigDecimal,
552    ) -> Result<(), SerdeError> {
553        self.write_arbitrary_precision(schema, value.as_ref())
554    }
555
556    fn write_string(&mut self, schema: &Schema<'_>, value: &str) -> Result<(), SerdeError> {
557        use crate::escape::escape_string;
558        if self.expecting_map_key {
559            // Map key: comma before (if not first entry), then "key":
560            if self.needs_comma {
561                self.output.push(',');
562            }
563            self.output.push('"');
564            self.output.push_str(&escape_string(value));
565            self.output.push_str("\":");
566            // The next write is the value — no comma before it
567            self.needs_comma = false;
568            self.expecting_map_key = false;
569        } else {
570            self.prefix(schema);
571            self.output.push('"');
572            self.output.push_str(&escape_string(value));
573            self.output.push('"');
574        }
575        Ok(())
576    }
577
578    fn write_blob(
579        &mut self,
580        schema: &Schema<'_>,
581        value: aws_smithy_types::Blob,
582    ) -> Result<(), SerdeError> {
583        use aws_smithy_types::base64;
584        self.prefix(schema);
585        let encoded = base64::encode(value.as_ref());
586        self.output.push('"');
587        self.output.push_str(&encoded);
588        self.output.push('"');
589        Ok(())
590    }
591
592    fn write_timestamp(&mut self, schema: &Schema<'_>, value: &DateTime) -> Result<(), SerdeError> {
593        self.prefix(schema);
594        let format = self.get_timestamp_format(schema)?;
595        let formatted = value
596            .fmt(format)
597            .map_err(|e| SerdeError::write_failed(format!("failed to format timestamp: {e}")))?;
598
599        match format {
600            TimestampFormat::EpochSeconds => {
601                // Epoch seconds as number
602                self.output.push_str(&formatted);
603            }
604            _ => {
605                // Other formats as strings
606                self.output.push('"');
607                self.output.push_str(&formatted);
608                self.output.push('"');
609            }
610        }
611        Ok(())
612    }
613
614    fn write_document(&mut self, schema: &Schema<'_>, value: &Document) -> Result<(), SerdeError> {
615        self.prefix(schema);
616        self.write_json_value(value)?;
617        Ok(())
618    }
619
620    fn write_null(&mut self, schema: &Schema<'_>) -> Result<(), SerdeError> {
621        self.prefix(schema);
622        self.output.push_str("null");
623        Ok(())
624    }
625}
626
627#[cfg(test)]
628mod tests {
629    use super::*;
630    use aws_smithy_schema::prelude::*;
631    use aws_smithy_schema::ShapeType;
632
633    #[test]
634    fn test_write_boolean() {
635        let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
636        ser.write_boolean(&BOOLEAN, true).unwrap();
637        let output = ser.finish();
638        assert_eq!(String::from_utf8(output).unwrap(), "true");
639    }
640
641    #[test]
642    fn test_write_string() {
643        let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
644        ser.write_string(&STRING, "hello").unwrap();
645        let output = ser.finish();
646        assert_eq!(String::from_utf8(output).unwrap(), "\"hello\"");
647    }
648
649    #[test]
650    fn test_write_integer() {
651        let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
652        ser.write_integer(&INTEGER, 42).unwrap();
653        let output = ser.finish();
654        assert_eq!(String::from_utf8(output).unwrap(), "42");
655    }
656
657    #[test]
658    fn floats_preserve_decimal_points_and_original_precision() {
659        let list = Schema::new(
660            aws_smithy_schema::shape_id!("test", "Floats"),
661            ShapeType::List,
662        );
663        let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
664        ser.write_list(&list, &|ser| {
665            ser.write_float(&FLOAT, 1.0)?;
666            ser.write_double(&DOUBLE, 1.0)?;
667            ser.write_float(&FLOAT, 0.0)?;
668            ser.write_double(&DOUBLE, 0.0)?;
669            ser.write_float(&FLOAT, -0.0)?;
670            ser.write_double(&DOUBLE, -0.0)?;
671            ser.write_float(&FLOAT, 1.01)?;
672            ser.write_double(&DOUBLE, 1.01)?;
673            ser.write_float(&FLOAT, 3.15)?;
674            ser.write_double(&DOUBLE, 3.15)?;
675            ser.write_float(&FLOAT, f32::INFINITY)?;
676            ser.write_float(&FLOAT, f32::NEG_INFINITY)?;
677            ser.write_float(&FLOAT, f32::NAN)?;
678            ser.write_double(&DOUBLE, f64::INFINITY)?;
679            ser.write_double(&DOUBLE, f64::NEG_INFINITY)?;
680            ser.write_double(&DOUBLE, f64::NAN)?;
681            Ok(())
682        })
683        .unwrap();
684        let output = String::from_utf8(ser.finish()).unwrap();
685        assert_eq!(
686            output,
687            r#"[1.0,1.0,0.0,0.0,-0.0,-0.0,1.01,1.01,3.15,3.15,"Infinity","-Infinity","NaN","Infinity","-Infinity","NaN"]"#
688        );
689    }
690
691    #[test]
692    fn test_write_null() {
693        let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
694        ser.write_null(&STRING).unwrap();
695        let output = ser.finish();
696        assert_eq!(String::from_utf8(output).unwrap(), "null");
697    }
698
699    #[test]
700    fn write_discriminated_document_emits_absolute_type() {
701        let mut obj = std::collections::HashMap::new();
702        obj.insert(
703            "greeting".to_string(),
704            Document::String("hello".to_string()),
705        );
706        let dd = DiscriminatedDocument::new(Document::Object(obj))
707            .with_discriminator("com.example#Greeting");
708        let schema = aws_smithy_schema::Schema::new(
709            aws_smithy_schema::shape_id!("com.example", "Greeting"),
710            ShapeType::Structure,
711        );
712        let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
713        ser.write_discriminated_document(&schema, &dd).unwrap();
714        let out = String::from_utf8(ser.finish()).unwrap();
715        assert_eq!(
716            out,
717            r#"{"__type":"com.example#Greeting","greeting":"hello"}"#
718        );
719    }
720
721    // A relative discriminator constructed via the test-only unchecked
722    // setter (bypassing the absolute-FQN check in
723    // `with_discriminator`) must still be caught at the wire-write site
724    // in debug builds.
725    #[cfg(debug_assertions)]
726    #[test]
727    #[should_panic(expected = "absolute shape id")]
728    fn write_discriminated_document_rejects_relative_discriminator() {
729        let mut obj = std::collections::HashMap::new();
730        obj.insert(
731            "greeting".to_string(),
732            Document::String("hello".to_string()),
733        );
734        let mut dd = DiscriminatedDocument::new(Document::Object(obj));
735        dd.set_discriminator_unchecked("Greeting");
736        let schema = aws_smithy_schema::Schema::new(
737            aws_smithy_schema::shape_id!("com.example", "Greeting"),
738            ShapeType::Structure,
739        );
740        let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
741        let _ = ser.write_discriminated_document(&schema, &dd);
742    }
743
744    #[test]
745    fn test_write_list() {
746        let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
747        let list_schema = aws_smithy_schema::Schema::new(
748            aws_smithy_schema::shape_id!("test", "List"),
749            aws_smithy_schema::ShapeType::List,
750        );
751        ser.write_list(&list_schema, &|s: &mut dyn ShapeSerializer| {
752            s.write_integer(&INTEGER, 1)?;
753            s.write_integer(&INTEGER, 2)?;
754            s.write_integer(&INTEGER, 3)?;
755            Ok(())
756        })
757        .unwrap();
758        let output = String::from_utf8(ser.finish()).unwrap();
759        assert_eq!(output, "[1,2,3]");
760    }
761
762    #[test]
763    fn test_write_full_object() {
764        use aws_smithy_schema::serde::SerializableStruct;
765
766        static ACTIVE_MEMBER: Schema<'static> = Schema::new_member(
767            aws_smithy_schema::shape_id!("test", "Struct"),
768            aws_smithy_schema::ShapeType::Boolean,
769            "active",
770            0,
771        );
772        static NAME_MEMBER: Schema<'static> = Schema::new_member(
773            aws_smithy_schema::shape_id!("test", "Struct"),
774            aws_smithy_schema::ShapeType::String,
775            "name",
776            1,
777        );
778        static COUNT_MEMBER: Schema<'static> = Schema::new_member(
779            aws_smithy_schema::shape_id!("test", "Struct"),
780            aws_smithy_schema::ShapeType::Integer,
781            "count",
782            2,
783        );
784        static PRICE_MEMBER: Schema<'static> = Schema::new_member(
785            aws_smithy_schema::shape_id!("test", "Struct"),
786            aws_smithy_schema::ShapeType::Float,
787            "price",
788            3,
789        );
790        static ITEMS_MEMBER: Schema<'static> = Schema::new_member(
791            aws_smithy_schema::shape_id!("test", "Struct"),
792            aws_smithy_schema::ShapeType::List,
793            "items",
794            4,
795        );
796
797        struct TestObject;
798        impl SerializableStruct for TestObject {
799            fn schema(&self) -> &Schema<'_> {
800                static SCHEMA: Schema<'static> = Schema::new_struct(
801                    aws_smithy_schema::shape_id!("test", "Struct"),
802                    aws_smithy_schema::ShapeType::Structure,
803                    &[
804                        &ACTIVE_MEMBER,
805                        &NAME_MEMBER,
806                        &COUNT_MEMBER,
807                        &PRICE_MEMBER,
808                        &ITEMS_MEMBER,
809                    ],
810                );
811                &SCHEMA
812            }
813
814            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
815                s.write_boolean(&ACTIVE_MEMBER, true)?;
816                s.write_string(&NAME_MEMBER, "test")?;
817                s.write_integer(&COUNT_MEMBER, 42)?;
818                s.write_float(&PRICE_MEMBER, 3.15)?;
819                s.write_list(&ITEMS_MEMBER, &|s| {
820                    s.write_integer(&INTEGER, 1)?;
821                    s.write_integer(&INTEGER, 2)?;
822                    Ok(())
823                })?;
824                Ok(())
825            }
826        }
827
828        let struct_schema = aws_smithy_schema::Schema::new(
829            aws_smithy_schema::shape_id!("test", "Struct"),
830            aws_smithy_schema::ShapeType::Structure,
831        );
832        let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
833        ser.write_struct(&struct_schema, &TestObject).unwrap();
834        let output = String::from_utf8(ser.finish()).unwrap();
835        assert_eq!(
836            output,
837            r#"{"active":true,"name":"test","count":42,"price":3.15,"items":[1,2]}"#
838        );
839    }
840
841    #[test]
842    fn test_nested_complex_serialization() {
843        use aws_smithy_schema::serde::SerializableStruct;
844
845        // Member schemas
846        static ID: Schema<'static> = Schema::new_member(
847            aws_smithy_schema::shape_id!("test", "User"),
848            aws_smithy_schema::ShapeType::Long,
849            "id",
850            0,
851        );
852        static NAME: Schema<'static> = Schema::new_member(
853            aws_smithy_schema::shape_id!("test", "User"),
854            aws_smithy_schema::ShapeType::String,
855            "name",
856            1,
857        );
858        static SCORES: Schema<'static> = Schema::new_member(
859            aws_smithy_schema::shape_id!("test", "User"),
860            aws_smithy_schema::ShapeType::List,
861            "scores",
862            2,
863        );
864        static ADDRESS: Schema<'static> = Schema::new_member(
865            aws_smithy_schema::shape_id!("test", "User"),
866            aws_smithy_schema::ShapeType::Structure,
867            "address",
868            3,
869        );
870        static COMPANIES: Schema<'static> = Schema::new_member(
871            aws_smithy_schema::shape_id!("test", "User"),
872            aws_smithy_schema::ShapeType::List,
873            "companies",
874            4,
875        );
876        static TAGS: Schema<'static> = Schema::new_member(
877            aws_smithy_schema::shape_id!("test", "User"),
878            aws_smithy_schema::ShapeType::Map,
879            "tags",
880            5,
881        );
882        static STREET: Schema<'static> = Schema::new_member(
883            aws_smithy_schema::shape_id!("test", "Address"),
884            aws_smithy_schema::ShapeType::String,
885            "street",
886            0,
887        );
888        static CITY: Schema<'static> = Schema::new_member(
889            aws_smithy_schema::shape_id!("test", "Address"),
890            aws_smithy_schema::ShapeType::String,
891            "city",
892            1,
893        );
894        static ZIP: Schema<'static> = Schema::new_member(
895            aws_smithy_schema::shape_id!("test", "Address"),
896            aws_smithy_schema::ShapeType::Integer,
897            "zip",
898            2,
899        );
900        static COMP_NAME: Schema<'static> = Schema::new_member(
901            aws_smithy_schema::shape_id!("test", "Company"),
902            aws_smithy_schema::ShapeType::String,
903            "name",
904            0,
905        );
906        static EMPLOYEES: Schema<'static> = Schema::new_member(
907            aws_smithy_schema::shape_id!("test", "Company"),
908            aws_smithy_schema::ShapeType::List,
909            "employees",
910            1,
911        );
912        static METADATA: Schema<'static> = Schema::new_member(
913            aws_smithy_schema::shape_id!("test", "Company"),
914            aws_smithy_schema::ShapeType::Map,
915            "metadata",
916            2,
917        );
918        static ACTIVE: Schema<'static> = Schema::new_member(
919            aws_smithy_schema::shape_id!("test", "Company"),
920            aws_smithy_schema::ShapeType::Boolean,
921            "active",
922            3,
923        );
924
925        struct AddressStruct;
926        impl SerializableStruct for AddressStruct {
927            fn schema(&self) -> &Schema<'_> {
928                static SCHEMA: Schema<'static> = Schema::new_struct(
929                    aws_smithy_schema::shape_id!("test", "Address"),
930                    aws_smithy_schema::ShapeType::Structure,
931                    &[&STREET, &CITY, &ZIP],
932                );
933                &SCHEMA
934            }
935
936            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
937                s.write_string(&STREET, "123 Main St")?;
938                s.write_string(&CITY, "Seattle")?;
939                s.write_integer(&ZIP, 98101)?;
940                Ok(())
941            }
942        }
943
944        struct CompanyStruct {
945            name: &'static str,
946            employees: &'static [&'static str],
947            metadata: &'static [(&'static str, i32)],
948            active: bool,
949        }
950        impl SerializableStruct for CompanyStruct {
951            fn schema(&self) -> &Schema<'_> {
952                static SCHEMA: Schema<'static> = Schema::new_struct(
953                    aws_smithy_schema::shape_id!("test", "Company"),
954                    aws_smithy_schema::ShapeType::Structure,
955                    &[&COMP_NAME, &EMPLOYEES, &METADATA, &ACTIVE],
956                );
957                &SCHEMA
958            }
959
960            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
961                s.write_string(&COMP_NAME, self.name)?;
962                s.write_list(&EMPLOYEES, &|s| {
963                    for e in self.employees {
964                        s.write_string(&STRING, e)?;
965                    }
966                    Ok(())
967                })?;
968                s.write_map(&METADATA, &|s| {
969                    for (k, v) in self.metadata {
970                        s.write_string(&::aws_smithy_schema::prelude::STRING, k)?;
971                        s.write_integer(&::aws_smithy_schema::prelude::INTEGER, *v)?;
972                    }
973                    Ok(())
974                })?;
975                s.write_boolean(&ACTIVE, self.active)?;
976                Ok(())
977            }
978        }
979
980        struct UserStruct;
981        impl SerializableStruct for UserStruct {
982            fn schema(&self) -> &Schema<'_> {
983                static SCHEMA: Schema<'static> = Schema::new_struct(
984                    aws_smithy_schema::shape_id!("test", "User"),
985                    aws_smithy_schema::ShapeType::Structure,
986                    &[&ID, &NAME, &SCORES, &ADDRESS, &COMPANIES, &TAGS],
987                );
988                &SCHEMA
989            }
990
991            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
992                s.write_long(&ID, 12345)?;
993                s.write_string(&NAME, "John Doe")?;
994                s.write_list(&SCORES, &|s| {
995                    s.write_double(&DOUBLE, 95.5)?;
996                    s.write_double(&DOUBLE, 87.3)?;
997                    s.write_double(&DOUBLE, 92.1)?;
998                    Ok(())
999                })?;
1000                s.write_struct(&ADDRESS, &AddressStruct)?;
1001                s.write_list(&COMPANIES, &|s| {
1002                    let struct_schema = Schema::new(
1003                        aws_smithy_schema::shape_id!("test", "Company"),
1004                        aws_smithy_schema::ShapeType::Structure,
1005                    );
1006                    s.write_struct(
1007                        &struct_schema,
1008                        &CompanyStruct {
1009                            name: "TechCorp",
1010                            employees: &["Alice", "Bob"],
1011                            metadata: &[("founded", 2010), ("size", 500)],
1012                            active: true,
1013                        },
1014                    )?;
1015                    s.write_struct(
1016                        &struct_schema,
1017                        &CompanyStruct {
1018                            name: "StartupInc",
1019                            employees: &["Charlie"],
1020                            metadata: &[("founded", 2020)],
1021                            active: false,
1022                        },
1023                    )?;
1024                    Ok(())
1025                })?;
1026                s.write_map(&TAGS, &|s| {
1027                    s.write_string(&::aws_smithy_schema::prelude::STRING, "role")?;
1028                    s.write_string(&::aws_smithy_schema::prelude::STRING, "admin")?;
1029                    s.write_string(&::aws_smithy_schema::prelude::STRING, "level")?;
1030                    s.write_string(&::aws_smithy_schema::prelude::STRING, "senior")?;
1031                    Ok(())
1032                })?;
1033                Ok(())
1034            }
1035        }
1036
1037        let struct_schema = Schema::new(
1038            aws_smithy_schema::shape_id!("test", "User"),
1039            aws_smithy_schema::ShapeType::Structure,
1040        );
1041        let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
1042        ser.write_struct(&struct_schema, &UserStruct).unwrap();
1043        let output = ser.finish();
1044        // Expected compact JSON (br# avoids escape noise)
1045        let expected: &[u8] = br#"{"id":12345,"name":"John Doe","scores":[95.5,87.3,92.1],"address":{"street":"123 Main St","city":"Seattle","zip":98101},"companies":[{"name":"TechCorp","employees":["Alice","Bob"],"metadata":{"founded":2010,"size":500},"active":true},{"name":"StartupInc","employees":["Charlie"],"metadata":{"founded":2020},"active":false}],"tags":{"role":"admin","level":"senior"}}"#;
1046        assert_eq!(output, expected);
1047    }
1048
1049    #[test]
1050    fn test_json_name_serialization() {
1051        use aws_smithy_schema::serde::SerializableStruct;
1052
1053        static FOO_MEMBER: Schema<'static> = Schema::new_member(
1054            aws_smithy_schema::shape_id!("test", "MyStruct"),
1055            aws_smithy_schema::ShapeType::String,
1056            "foo",
1057            0,
1058        );
1059        // bar has @jsonName("Baz")
1060        static BAR_MEMBER: Schema<'static> = Schema::new_member(
1061            aws_smithy_schema::shape_id!("test", "MyStruct"),
1062            aws_smithy_schema::ShapeType::Integer,
1063            "bar",
1064            1,
1065        )
1066        .with_json_name("Baz");
1067
1068        struct TestStruct;
1069        impl SerializableStruct for TestStruct {
1070            fn schema(&self) -> &Schema<'_> {
1071                static SCHEMA: Schema<'static> = Schema::new_struct(
1072                    aws_smithy_schema::shape_id!("test", "MyStruct"),
1073                    aws_smithy_schema::ShapeType::Structure,
1074                    &[&FOO_MEMBER, &BAR_MEMBER],
1075                );
1076                &SCHEMA
1077            }
1078
1079            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1080                s.write_string(&FOO_MEMBER, "hello")?;
1081                s.write_integer(&BAR_MEMBER, 42)?;
1082                Ok(())
1083            }
1084        }
1085
1086        // With use_json_name=true (default), "bar" should serialize as "Baz"
1087        let struct_schema = Schema::new(
1088            aws_smithy_schema::shape_id!("test", "MyStruct"),
1089            aws_smithy_schema::ShapeType::Structure,
1090        );
1091        let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
1092        ser.write_struct(&struct_schema, &TestStruct).unwrap();
1093        let output = String::from_utf8(ser.finish()).unwrap();
1094        assert_eq!(output, r#"{"foo":"hello","Baz":42}"#);
1095
1096        // With use_json_name=false, "bar" should stay as "bar"
1097        let mut ser = JsonSerializer::new(Arc::new(
1098            JsonCodecSettings::builder().use_json_name(false).build(),
1099        ));
1100        ser.write_struct(&struct_schema, &TestStruct).unwrap();
1101        let output = String::from_utf8(ser.finish()).unwrap();
1102        assert_eq!(output, r#"{"foo":"hello","bar":42}"#);
1103    }
1104
1105    /// The same `@jsonName` behaviour, but driven by a schema materialized at
1106    /// runtime from strings a local arena owns rather than by codegen-emitted
1107    /// `'static` literals. This is the property the Serialization and Schema
1108    /// Decoupling SEP requires: schema-driven serde must work identically
1109    /// regardless of where the schema came from.
1110    #[test]
1111    fn test_json_name_serialization_from_runtime_schema() {
1112        use aws_smithy_schema::serde::SerializableStruct;
1113
1114        // Arena the caller owns. Nothing is 'static; nothing is leaked.
1115        let arena: Vec<String> = vec![
1116            String::from("bar"),        // member name
1117            String::from("RuntimeBaz"), // @jsonName
1118        ];
1119
1120        let bar_member: Schema<'_> = Schema::new_member(
1121            aws_smithy_schema::shape_id!("test", "MyStruct"),
1122            aws_smithy_schema::ShapeType::Integer,
1123            &arena[0],
1124            0,
1125        )
1126        .with_json_name(&arena[1]);
1127
1128        // The struct schema is runtime-built too, so the value reports it by borrowing.
1129        let members = [&bar_member];
1130        let struct_schema = Schema::new_struct(
1131            aws_smithy_schema::shape_id!("test", "MyStruct"),
1132            aws_smithy_schema::ShapeType::Structure,
1133            &members,
1134        );
1135
1136        struct RuntimeStruct<'a> {
1137            schema: &'a Schema<'a>,
1138            member: &'a Schema<'a>,
1139        }
1140        impl SerializableStruct for RuntimeStruct<'_> {
1141            fn schema(&self) -> &Schema<'_> {
1142                self.schema
1143            }
1144
1145            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1146                s.write_integer(self.member, 42)
1147            }
1148        }
1149        let value = RuntimeStruct {
1150            schema: &struct_schema,
1151            member: &bar_member,
1152        };
1153        assert!(std::ptr::eq(value.schema(), &struct_schema));
1154
1155        // The runtime @jsonName must win over the runtime member name.
1156        let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
1157        ser.write_struct(&struct_schema, &value).unwrap();
1158        let output = String::from_utf8(ser.finish()).unwrap();
1159        assert_eq!(output, r#"{"RuntimeBaz":42}"#);
1160
1161        // And the opt-out still falls back to the runtime member name.
1162        let mut ser = JsonSerializer::new(Arc::new(
1163            JsonCodecSettings::builder().use_json_name(false).build(),
1164        ));
1165        ser.write_struct(&struct_schema, &value).unwrap();
1166        let output = String::from_utf8(ser.finish()).unwrap();
1167        assert_eq!(output, r#"{"bar":42}"#);
1168    }
1169
1170    /// Field names are escaped once, when their key is cached on the schema. A member
1171    /// name or `@jsonName` that needs escaping must still produce valid JSON, on the
1172    /// first serialization and on later ones that reuse the cached key.
1173    #[test]
1174    fn cached_field_keys_are_escaped() {
1175        use aws_smithy_schema::serde::SerializableStruct;
1176
1177        let names: Vec<String> = vec![
1178            String::from("quote\"d"),         // member name
1179            String::from("back\\slash"),      // member name
1180            String::from("line\nbreak\u{1}"), // @jsonName
1181        ];
1182        let quoted = Schema::new_member(
1183            aws_smithy_schema::shape_id!("test", "Escapes"),
1184            ShapeType::String,
1185            &names[0],
1186            0,
1187        );
1188        let renamed = Schema::new_member(
1189            aws_smithy_schema::shape_id!("test", "Escapes"),
1190            ShapeType::String,
1191            &names[1],
1192            1,
1193        )
1194        .with_json_name(&names[2]);
1195        let members = [&quoted, &renamed];
1196        let struct_schema = Schema::new_struct(
1197            aws_smithy_schema::shape_id!("test", "Escapes"),
1198            ShapeType::Structure,
1199            &members,
1200        );
1201
1202        struct Escapes<'a>(&'a Schema<'a>, &'a Schema<'a>, &'a Schema<'a>);
1203        impl SerializableStruct for Escapes<'_> {
1204            fn schema(&self) -> &Schema<'_> {
1205                self.0
1206            }
1207
1208            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1209                s.write_string(self.1, "a")?;
1210                s.write_string(self.2, "b")
1211            }
1212        }
1213        let value = Escapes(&struct_schema, &quoted, &renamed);
1214
1215        for _ in 0..2 {
1216            let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
1217            ser.write_struct(&struct_schema, &value).unwrap();
1218            let output = String::from_utf8(ser.finish()).unwrap();
1219            assert_eq!(output, r#"{"quote\"d":"a","line\nbreak\u0001":"b"}"#);
1220            let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
1221            assert_eq!(parsed["quote\"d"], "a");
1222            assert_eq!(parsed["line\nbreak\u{1}"], "b");
1223
1224            let mut ser = JsonSerializer::new(Arc::new(
1225                JsonCodecSettings::builder().use_json_name(false).build(),
1226            ));
1227            ser.write_struct(&struct_schema, &value).unwrap();
1228            let output = String::from_utf8(ser.finish()).unwrap();
1229            assert_eq!(output, r#"{"quote\"d":"a","back\\slash":"b"}"#);
1230        }
1231    }
1232
1233    /// A schema is shared by every codec in the process, so the key cached on it must
1234    /// not depend on the settings of whichever codec reached it first. The existing
1235    /// `@jsonName` test reaches its schemas with `use_json_name(true)` first; this one
1236    /// starts with `use_json_name(false)`.
1237    #[test]
1238    fn cached_field_keys_do_not_depend_on_which_codec_was_first() {
1239        use aws_smithy_schema::serde::SerializableStruct;
1240
1241        static RENAMED: Schema<'static> = Schema::new_member(
1242            aws_smithy_schema::shape_id!("test", "FirstCodec"),
1243            ShapeType::String,
1244            "memberName",
1245            0,
1246        )
1247        .with_json_name("wireName");
1248        static STRUCT: Schema<'static> = Schema::new_struct(
1249            aws_smithy_schema::shape_id!("test", "FirstCodec"),
1250            ShapeType::Structure,
1251            &[&RENAMED],
1252        );
1253
1254        struct FirstCodec;
1255        impl SerializableStruct for FirstCodec {
1256            fn schema(&self) -> &Schema<'_> {
1257                &STRUCT
1258            }
1259
1260            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1261                s.write_string(&RENAMED, "v")
1262            }
1263        }
1264
1265        let serialize = |use_json_name: bool| {
1266            let mut ser = JsonSerializer::new(Arc::new(
1267                JsonCodecSettings::builder()
1268                    .use_json_name(use_json_name)
1269                    .build(),
1270            ));
1271            ser.write_struct(&STRUCT, &FirstCodec).unwrap();
1272            String::from_utf8(ser.finish()).unwrap()
1273        };
1274        assert_eq!(serialize(false), r#"{"memberName":"v"}"#);
1275        assert_eq!(serialize(true), r#"{"wireName":"v"}"#);
1276        assert_eq!(serialize(false), r#"{"memberName":"v"}"#);
1277    }
1278
1279    #[test]
1280    fn struct_inside_map_serializes_member_names_correctly() {
1281        // Regression test: when a struct is a map value, the map's expecting_map_key
1282        // flag must not leak into the struct's member serialization.
1283        use aws_smithy_schema::serde::{SerializableStruct, ShapeSerializer};
1284
1285        static INNER_NAME: Schema<'static> = Schema::new_member(
1286            aws_smithy_schema::shape_id!("test", "Inner"),
1287            ShapeType::String,
1288            "name",
1289            0,
1290        );
1291        static INNER_MEMBERS: &[&Schema<'_>] = &[&INNER_NAME];
1292        static INNER_SCHEMA: Schema<'static> = Schema::new_struct(
1293            aws_smithy_schema::shape_id!("test", "Inner"),
1294            ShapeType::Structure,
1295            INNER_MEMBERS,
1296        );
1297
1298        struct Inner;
1299        impl SerializableStruct for Inner {
1300            fn schema(&self) -> &Schema<'_> {
1301                &INNER_SCHEMA
1302            }
1303
1304            fn serialize_members(
1305                &self,
1306                ser: &mut dyn ShapeSerializer,
1307            ) -> Result<(), aws_smithy_schema::serde::SerdeError> {
1308                ser.write_string(&INNER_NAME, "Alice")
1309            }
1310        }
1311
1312        static MAP_SCHEMA: Schema<'static> = Schema::new(
1313            aws_smithy_schema::shape_id!("test", "MyMap"),
1314            ShapeType::Map,
1315        );
1316
1317        let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
1318        ser.write_map(&MAP_SCHEMA, &|ser| {
1319            ser.write_string(&aws_smithy_schema::prelude::STRING, "key1")?;
1320            ser.write_struct(&INNER_SCHEMA, &Inner)?;
1321            Ok(())
1322        })
1323        .unwrap();
1324        let output = String::from_utf8(ser.finish()).unwrap();
1325        assert_eq!(output, r#"{"key1":{"name":"Alice"}}"#);
1326    }
1327
1328    /// Regression test: when a member schema (e.g. one with
1329    /// `member_name = "value"`) is passed to `write_map` from inside
1330    /// another map's value position, the inner `write_map` call must NOT
1331    /// emit `"value":` before the inner `{...}`.
1332    ///
1333    /// Without context-aware prefix suppression, the output would be
1334    /// `{"outer_k":"value":{"inner_k":"inner_v"}}` (invalid JSON).
1335    /// With the fix, output is `{"outer_k":{"inner_k":"inner_v"}}`.
1336    #[test]
1337    fn map_value_with_value_member_schema_does_not_leak_member_name() {
1338        static OUTER_VALUE: Schema<'static> = Schema::new_member(
1339            aws_smithy_schema::shape_id!("test", "OuterMap", "value"),
1340            ShapeType::Map,
1341            "value",
1342            1,
1343        );
1344
1345        let outer_map_schema = Schema::new(
1346            aws_smithy_schema::shape_id!("test", "OuterMap"),
1347            ShapeType::Map,
1348        );
1349        let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
1350        ser.write_map(&outer_map_schema, &|s| {
1351            s.write_string(&aws_smithy_schema::prelude::STRING, "outer_k")?;
1352            s.write_map(&OUTER_VALUE, &|s| {
1353                s.write_string(&aws_smithy_schema::prelude::STRING, "inner_k")?;
1354                s.write_string(&aws_smithy_schema::prelude::STRING, "inner_v")?;
1355                Ok(())
1356            })?;
1357            Ok(())
1358        })
1359        .unwrap();
1360        let output = String::from_utf8(ser.finish()).unwrap();
1361        assert_eq!(output, r#"{"outer_k":{"inner_k":"inner_v"}}"#);
1362    }
1363
1364    /// Regression test mirroring the map-of-map case for list-of-map: the
1365    /// inner map is reached via a member schema whose member name is
1366    /// "member" (the list's element position). That member name must not
1367    /// leak into the JSON output.
1368    #[test]
1369    fn list_element_with_member_member_schema_does_not_leak_member_name() {
1370        static OUTER_MEMBER: Schema<'static> = Schema::new_member(
1371            aws_smithy_schema::shape_id!("test", "OuterList", "member"),
1372            ShapeType::Map,
1373            "member",
1374            0,
1375        );
1376
1377        let outer_list_schema = Schema::new(
1378            aws_smithy_schema::shape_id!("test", "OuterList"),
1379            ShapeType::List,
1380        );
1381        let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
1382        ser.write_list(&outer_list_schema, &|s| {
1383            s.write_map(&OUTER_MEMBER, &|s| {
1384                s.write_string(&aws_smithy_schema::prelude::STRING, "k1")?;
1385                s.write_string(&aws_smithy_schema::prelude::STRING, "v1")?;
1386                Ok(())
1387            })?;
1388            Ok(())
1389        })
1390        .unwrap();
1391        let output = String::from_utf8(ser.finish()).unwrap();
1392        assert_eq!(output, r#"[{"k1":"v1"}]"#);
1393    }
1394
1395    /// List-of-list with a member schema for the inner list element.
1396    /// Verifies the suppression also applies to nested `write_list`.
1397    #[test]
1398    fn nested_list_with_member_member_schema_does_not_leak_member_name() {
1399        static OUTER_MEMBER: Schema<'static> = Schema::new_member(
1400            aws_smithy_schema::shape_id!("test", "OuterList", "member"),
1401            ShapeType::List,
1402            "member",
1403            0,
1404        );
1405
1406        let outer_list_schema = Schema::new(
1407            aws_smithy_schema::shape_id!("test", "OuterList"),
1408            ShapeType::List,
1409        );
1410        let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
1411        ser.write_list(&outer_list_schema, &|s| {
1412            s.write_list(&OUTER_MEMBER, &|s| {
1413                s.write_integer(&aws_smithy_schema::prelude::INTEGER, 1)?;
1414                s.write_integer(&aws_smithy_schema::prelude::INTEGER, 2)?;
1415                Ok(())
1416            })?;
1417            Ok(())
1418        })
1419        .unwrap();
1420        let output = String::from_utf8(ser.finish()).unwrap();
1421        assert_eq!(output, r#"[[1,2]]"#);
1422    }
1423
1424    /// Positive control: the parent struct context is correctly restored
1425    /// after a non-struct child exits. A struct with `[string, map, string]`
1426    /// members must emit field names for ALL three — the suppression
1427    /// applied while writing the map's body must not bleed into the
1428    /// sibling string member that follows.
1429    #[test]
1430    fn struct_context_restored_after_nested_aggregate_member() {
1431        static A: Schema<'static> = Schema::new_member(
1432            aws_smithy_schema::shape_id!("test", "Outer"),
1433            ShapeType::String,
1434            "a",
1435            0,
1436        );
1437        static B: Schema<'static> = Schema::new_member(
1438            aws_smithy_schema::shape_id!("test", "Outer"),
1439            ShapeType::Map,
1440            "b",
1441            1,
1442        );
1443        static C: Schema<'static> = Schema::new_member(
1444            aws_smithy_schema::shape_id!("test", "Outer"),
1445            ShapeType::String,
1446            "c",
1447            2,
1448        );
1449
1450        struct Outer;
1451        impl SerializableStruct for Outer {
1452            fn schema(&self) -> &Schema<'_> {
1453                static SCHEMA: Schema<'static> = Schema::new_struct(
1454                    aws_smithy_schema::shape_id!("test", "Outer"),
1455                    aws_smithy_schema::ShapeType::Structure,
1456                    &[&A, &B, &C],
1457                );
1458                &SCHEMA
1459            }
1460
1461            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1462                s.write_string(&A, "1")?;
1463                s.write_map(&B, &|s| {
1464                    s.write_string(&aws_smithy_schema::prelude::STRING, "k")?;
1465                    s.write_string(&aws_smithy_schema::prelude::STRING, "v")?;
1466                    Ok(())
1467                })?;
1468                s.write_string(&C, "2")?;
1469                Ok(())
1470            }
1471        }
1472
1473        let outer_schema = Schema::new(
1474            aws_smithy_schema::shape_id!("test", "Outer"),
1475            ShapeType::Structure,
1476        );
1477        let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
1478        ser.write_struct(&outer_schema, &Outer).unwrap();
1479        let output = String::from_utf8(ser.finish()).unwrap();
1480        assert_eq!(output, r#"{"a":"1","b":{"k":"v"},"c":"2"}"#);
1481    }
1482
1483    /// `@jsonName` is part of the same `member_key(schema)` resolution
1484    /// pipeline that emits `member_name`, so context-aware suppression
1485    /// must apply equally to it. A member schema in non-struct context
1486    /// with `@jsonName` set must NOT emit the JSON name as a key.
1487    #[test]
1488    fn map_value_with_json_name_member_schema_does_not_leak_json_name() {
1489        static OUTER_VALUE: Schema<'static> = Schema::new_member(
1490            aws_smithy_schema::shape_id!("test", "OuterMap", "value"),
1491            ShapeType::String,
1492            "value",
1493            1,
1494        )
1495        .with_json_name("CustomName");
1496
1497        let outer_map_schema = Schema::new(
1498            aws_smithy_schema::shape_id!("test", "OuterMap"),
1499            ShapeType::Map,
1500        );
1501        let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
1502        ser.write_map(&outer_map_schema, &|s| {
1503            s.write_string(&aws_smithy_schema::prelude::STRING, "k")?;
1504            s.write_string(&OUTER_VALUE, "v")?;
1505            Ok(())
1506        })
1507        .unwrap();
1508        let output = String::from_utf8(ser.finish()).unwrap();
1509        assert_eq!(output, r#"{"k":"v"}"#);
1510    }
1511
1512    // --- write_big_integer / write_big_decimal gate on use_string_for_arbitrary_precision -------
1513
1514    fn settings_with_string_for_arbitrary_precision(value: bool) -> Arc<JsonCodecSettings> {
1515        Arc::new(
1516            JsonCodecSettings::builder()
1517                .use_string_for_arbitrary_precision(value)
1518                .build(),
1519        )
1520    }
1521
1522    #[test]
1523    fn write_big_integer_emits_a_number_by_default() {
1524        // Default (`use_string_for_arbitrary_precision = false`) emits a
1525        // bigInteger as a raw JSON number — interoperable with
1526        // arbitrary-precision parsers, and lossless.
1527        use std::str::FromStr;
1528        let bi = BigInteger::from_str("99999999999999999999999").unwrap();
1529        let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
1530        ser.write_big_integer(&aws_smithy_schema::prelude::BIG_INTEGER, &bi)
1531            .unwrap();
1532        let output = String::from_utf8(ser.finish()).unwrap();
1533        assert_eq!(output, "99999999999999999999999");
1534    }
1535
1536    #[test]
1537    fn write_big_integer_emits_a_string_when_setting_enabled() {
1538        use std::str::FromStr;
1539        let bi = BigInteger::from_str("99999999999999999999999").unwrap();
1540        let mut ser = JsonSerializer::new(settings_with_string_for_arbitrary_precision(true));
1541        ser.write_big_integer(&aws_smithy_schema::prelude::BIG_INTEGER, &bi)
1542            .unwrap();
1543        let output = String::from_utf8(ser.finish()).unwrap();
1544        assert_eq!(output, "\"99999999999999999999999\"");
1545    }
1546
1547    #[test]
1548    fn write_big_decimal_emits_a_number_by_default() {
1549        use std::str::FromStr;
1550        let bd = BigDecimal::from_str("1.234e500").unwrap();
1551        let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
1552        ser.write_big_decimal(&aws_smithy_schema::prelude::BIG_DECIMAL, &bd)
1553            .unwrap();
1554        let output = String::from_utf8(ser.finish()).unwrap();
1555        assert!(
1556            !output.starts_with('"'),
1557            "expected a raw JSON number, got {output}"
1558        );
1559        assert_ne!(output, "null");
1560    }
1561
1562    #[test]
1563    fn write_big_decimal_emits_a_string_when_setting_enabled() {
1564        use std::str::FromStr;
1565        let bd = BigDecimal::from_str("0.123456789012345678901234567890").unwrap();
1566        let mut ser = JsonSerializer::new(settings_with_string_for_arbitrary_precision(true));
1567        ser.write_big_decimal(&aws_smithy_schema::prelude::BIG_DECIMAL, &bd)
1568            .unwrap();
1569        let output = String::from_utf8(ser.finish()).unwrap();
1570        assert_eq!(output, "\"0.123456789012345678901234567890\"");
1571    }
1572
1573    // --- Raw-number emission validates the JSON number grammar ------------------
1574    //
1575    // `BigInteger::from_str` / `BigDecimal::from_str` are structurally
1576    // strict, but deliberately accept leading zeros for backward
1577    // compatibility. RFC 8259 does not, so these values are all publicly
1578    // constructible today *and* invalid as raw JSON. Emitting them verbatim
1579    // would produce JSON no parser accepts, so the raw path rewrites
1580    // them.
1581
1582    fn normalizing_settings() -> Arc<JsonCodecSettings> {
1583        Arc::new(
1584            JsonCodecSettings::builder()
1585                .normalize_arbitrary_precision(true)
1586                .build(),
1587        )
1588    }
1589
1590    #[test]
1591    fn non_json_numbers_are_rejected_on_the_raw_path_by_default() {
1592        use std::str::FromStr;
1593        for text in ["00123", "-01", "00.1", "+1.5", ".5", "1."] {
1594            let bd = BigDecimal::from_str(text).unwrap();
1595            let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
1596            let err = ser
1597                .write_big_decimal(&aws_smithy_schema::prelude::BIG_DECIMAL, &bd)
1598                .expect_err(&format!("expected {text:?} to be rejected by default"));
1599            assert!(
1600                matches!(err, SerdeError::InvalidInput { .. }),
1601                "{text:?}: {err:?}"
1602            );
1603            // Nothing partial was written before the rejection.
1604            assert_eq!(String::from_utf8(ser.finish()).unwrap(), "", "for {text:?}");
1605        }
1606    }
1607
1608    #[test]
1609    fn write_big_integer_normalizes_non_json_numbers_on_the_raw_path() {
1610        use std::str::FromStr;
1611        for (text, expected) in [
1612            ("00123", "123"),
1613            ("007", "7"),
1614            ("-01", "-1"),
1615            ("0000", "0"),
1616            ("-0000", "-0"),
1617        ] {
1618            // The (structurally strict) parser still accepts the input, so this is a value a
1619            // caller can actually hand to the serializer.
1620            let bi = BigInteger::from_str(text)
1621                .unwrap_or_else(|e| panic!("BigInteger::from_str({text:?}) should succeed: {e}"));
1622            let mut ser = JsonSerializer::new(normalizing_settings());
1623            ser.write_big_integer(&aws_smithy_schema::prelude::BIG_INTEGER, &bi)
1624                .unwrap_or_else(|e| panic!("{text:?} should be normalized: {e:?}"));
1625            assert_eq!(
1626                String::from_utf8(ser.finish()).unwrap(),
1627                expected,
1628                "for {text:?}"
1629            );
1630        }
1631    }
1632
1633    #[test]
1634    fn write_big_decimal_normalizes_non_json_numbers_on_the_raw_path() {
1635        use std::str::FromStr;
1636        for (text, expected) in [
1637            ("00123", "123"),
1638            ("007", "7"),
1639            ("-01", "-1"),
1640            ("00.1", "0.1"),
1641            ("-01.5", "-1.5"),
1642            ("007e2", "7e2"),
1643            ("0000", "0"),
1644            ("00.0", "0.0"),
1645            ("01e-3", "1e-3"),
1646            ("+000987654321", "987654321"),
1647            ("+1.5", "1.5"),
1648            (".5", "0.5"),
1649            ("1.", "1"),
1650        ] {
1651            let bd = BigDecimal::from_str(text)
1652                .unwrap_or_else(|e| panic!("BigDecimal::from_str({text:?}) should succeed: {e}"));
1653            let mut ser = JsonSerializer::new(normalizing_settings());
1654            ser.write_big_decimal(&aws_smithy_schema::prelude::BIG_DECIMAL, &bd)
1655                .unwrap_or_else(|e| panic!("{text:?} should be normalized: {e:?}"));
1656            assert_eq!(
1657                String::from_utf8(ser.finish()).unwrap(),
1658                expected,
1659                "for {text:?}"
1660            );
1661        }
1662    }
1663
1664    #[test]
1665    fn to_json_number_rejects_text_that_is_not_a_number() {
1666        for text in [
1667            "", "-", "+", ".", "abc", "1.2.3", "0x1f", "1e", "e1", "1 2", "NaN", "--1",
1668        ] {
1669            assert_eq!(to_json_number(text), None, "for {text:?}");
1670        }
1671    }
1672
1673    #[test]
1674    fn non_json_numbers_are_accepted_when_emitted_as_strings() {
1675        use std::str::FromStr;
1676        // The string path cannot produce malformed JSON, so it stays
1677        // permissive: the exact stored text round-trips, escaped. The
1678        // inputs are leading-zero forms — still constructible under the
1679        // strict `FromStr` grammar, but not RFC 8259 JSON numbers, so they
1680        // are exactly the values the raw path rejects.
1681        let bi = BigInteger::from_str("00123").unwrap();
1682        let mut ser = JsonSerializer::new(settings_with_string_for_arbitrary_precision(true));
1683        ser.write_big_integer(&aws_smithy_schema::prelude::BIG_INTEGER, &bi)
1684            .unwrap();
1685        assert_eq!(String::from_utf8(ser.finish()).unwrap(), "\"00123\"");
1686
1687        let bd = BigDecimal::from_str("00.1").unwrap();
1688        let mut ser = JsonSerializer::new(settings_with_string_for_arbitrary_precision(true));
1689        ser.write_big_decimal(&aws_smithy_schema::prelude::BIG_DECIMAL, &bd)
1690            .unwrap();
1691        assert_eq!(String::from_utf8(ser.finish()).unwrap(), "\"00.1\"");
1692    }
1693
1694    #[test]
1695    fn write_big_numbers_accept_every_json_number_form() {
1696        use std::str::FromStr;
1697        for text in [
1698            "0",
1699            "-0",
1700            "123",
1701            "-123",
1702            "99999999999999999999999",
1703            "1.5",
1704            "-0.5",
1705            "1e3",
1706            "1E3",
1707            "1e+9",
1708            "1e-9",
1709            "1.23e10",
1710            "1.23E-10",
1711            "0.123456789012345678901234567890",
1712        ] {
1713            let bd = BigDecimal::from_str(text).unwrap();
1714            let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
1715            ser.write_big_decimal(&aws_smithy_schema::prelude::BIG_DECIMAL, &bd)
1716                .unwrap_or_else(|e| panic!("expected {text:?} to serialize: {e}"));
1717            assert_eq!(
1718                String::from_utf8(ser.finish()).unwrap(),
1719                *text,
1720                "bigDecimal {text:?} must be emitted verbatim"
1721            );
1722        }
1723
1724        for text in ["0", "-0", "123", "-123", "99999999999999999999999"] {
1725            let bi = BigInteger::from_str(text).unwrap();
1726            let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
1727            ser.write_big_integer(&aws_smithy_schema::prelude::BIG_INTEGER, &bi)
1728                .unwrap_or_else(|e| panic!("expected {text:?} to serialize: {e}"));
1729            assert_eq!(String::from_utf8(ser.finish()).unwrap(), *text);
1730        }
1731    }
1732
1733    #[test]
1734    fn is_json_number_matches_the_rfc_8259_grammar() {
1735        for good in [
1736            "0",
1737            "-0",
1738            "1",
1739            "-1",
1740            "10",
1741            "1.0",
1742            "-1.25",
1743            "1e1",
1744            "1E1",
1745            "1e+1",
1746            "1e-1",
1747            "0.5e-10",
1748            "1234567890123456789012345678901234567890",
1749        ] {
1750            assert!(is_json_number(good), "expected {good:?} to be accepted");
1751        }
1752        for bad in [
1753            "", "-", "+", "+1", "01", "00", "-01", "1.", ".5", "1..2", "1.2.3", "--1", "1e", "1e+",
1754            "1e-", "e1", "1e2e3", "0x1f", "1 2", "Infinity", "NaN", "1,000",
1755        ] {
1756            assert!(!is_json_number(bad), "expected {bad:?} to be rejected");
1757        }
1758    }
1759
1760    // --- Required value-type members are serialized even when the value equals
1761    // the type's zero/default (e.g. bool `false`). This mirrors the generated
1762    // non-optional branch `{ let val = &self.x; ser.write_boolean(..) }` (always
1763    // written, never a skip-if-default), and shapes the ELB
1764    // `LoadBalancerAttributes { ConnectionDraining { Enabled = false } }` case:
1765    // the nested structure member is present and `enabled=false` appears.
1766
1767    static CD_ENABLED: Schema = Schema::new_member(
1768        aws_smithy_schema::shape_id!("test", "ConnectionDraining$enabled"),
1769        ShapeType::Boolean,
1770        "enabled",
1771        0,
1772    );
1773    static LBA_CD: Schema = Schema::new_member(
1774        aws_smithy_schema::shape_id!("test", "LoadBalancerAttributes$connectionDraining"),
1775        ShapeType::Structure,
1776        "connectionDraining",
1777        0,
1778    );
1779    static LBA_SCHEMA: Schema = Schema::new_struct(
1780        aws_smithy_schema::shape_id!("test", "LoadBalancerAttributes"),
1781        ShapeType::Structure,
1782        &[&LBA_CD],
1783    );
1784
1785    struct ConnectionDraining {
1786        enabled: bool,
1787    }
1788    impl SerializableStruct for ConnectionDraining {
1789        fn schema(&self) -> &Schema<'_> {
1790            static SCHEMA: Schema<'static> = Schema::new_struct(
1791                aws_smithy_schema::shape_id!("test", "ConnectionDraining"),
1792                aws_smithy_schema::ShapeType::Structure,
1793                &[&CD_ENABLED],
1794            );
1795            &SCHEMA
1796        }
1797
1798        fn serialize_members(&self, ser: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1799            // Non-optional value-type member: unconditional write (mirrors codegen).
1800            let val = &self.enabled;
1801            ser.write_boolean(&CD_ENABLED, *val)
1802        }
1803    }
1804    struct LoadBalancerAttributes {
1805        connection_draining: ConnectionDraining,
1806    }
1807    impl SerializableStruct for LoadBalancerAttributes {
1808        fn schema(&self) -> &Schema<'_> {
1809            &LBA_SCHEMA
1810        }
1811
1812        fn serialize_members(&self, ser: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1813            ser.write_struct(&LBA_CD, &self.connection_draining)
1814        }
1815    }
1816
1817    #[test]
1818    fn required_value_type_false_bool_is_serialized() {
1819        let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
1820        ser.write_struct(
1821            &LBA_SCHEMA,
1822            &LoadBalancerAttributes {
1823                connection_draining: ConnectionDraining { enabled: false },
1824            },
1825        )
1826        .unwrap();
1827        let out = String::from_utf8(ser.finish()).unwrap();
1828        assert_eq!(
1829            out, r#"{"connectionDraining":{"enabled":false}}"#,
1830            "the required-default value-type member must be present with false, not dropped"
1831        );
1832    }
1833
1834    // Tagged-union serialization parity: a union serializes as a single-entry
1835    // object keyed by the active variant name (`{"variantName": value}`), the
1836    // same logical shape the CBOR serializer produces. Covers a scalar variant,
1837    // a struct variant, and two union members in one input.
1838    #[test]
1839    fn tagged_union_serializes_as_single_variant_key() {
1840        static EP_ARN: Schema = Schema::new_member(
1841            aws_smithy_schema::shape_id!("test", "Endpoint"),
1842            ShapeType::String,
1843            "arn",
1844            0,
1845        );
1846        static EP_CONFIG: Schema = Schema::new_member(
1847            aws_smithy_schema::shape_id!("test", "Endpoint"),
1848            ShapeType::Structure,
1849            "config",
1850            1,
1851        );
1852        static ENDPOINT_SCHEMA: Schema = Schema::new_struct(
1853            aws_smithy_schema::shape_id!("test", "Endpoint"),
1854            ShapeType::Union,
1855            &[&EP_ARN, &EP_CONFIG],
1856        );
1857        static EC_NAME: Schema = Schema::new_member(
1858            aws_smithy_schema::shape_id!("test", "EndpointConfig"),
1859            ShapeType::String,
1860            "name",
1861            0,
1862        );
1863        static CC_ATTACH: Schema = Schema::new_member(
1864            aws_smithy_schema::shape_id!("test", "CreateConnectionInput"),
1865            ShapeType::Union,
1866            "attachPoint",
1867            0,
1868        );
1869        static CC_REMOTE: Schema = Schema::new_member(
1870            aws_smithy_schema::shape_id!("test", "CreateConnectionInput"),
1871            ShapeType::Union,
1872            "remoteAccount",
1873            1,
1874        );
1875        static INPUT_SCHEMA: Schema = Schema::new_struct(
1876            aws_smithy_schema::shape_id!("test", "CreateConnectionInput"),
1877            ShapeType::Structure,
1878            &[&CC_ATTACH, &CC_REMOTE],
1879        );
1880
1881        struct EndpointConfig {
1882            name: &'static str,
1883        }
1884        impl SerializableStruct for EndpointConfig {
1885            fn schema(&self) -> &Schema<'_> {
1886                static SCHEMA: Schema<'static> = Schema::new_struct(
1887                    aws_smithy_schema::shape_id!("test", "EndpointConfig"),
1888                    aws_smithy_schema::ShapeType::Structure,
1889                    &[&EC_NAME],
1890                );
1891                &SCHEMA
1892            }
1893
1894            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1895                s.write_string(&EC_NAME, self.name)
1896            }
1897        }
1898        enum Endpoint {
1899            Arn(&'static str),
1900            Config(EndpointConfig),
1901        }
1902        impl SerializableStruct for Endpoint {
1903            fn schema(&self) -> &Schema<'_> {
1904                &ENDPOINT_SCHEMA
1905            }
1906
1907            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1908                match self {
1909                    Endpoint::Arn(v) => s.write_string(&EP_ARN, v),
1910                    Endpoint::Config(v) => s.write_struct(&EP_CONFIG, v),
1911                }
1912            }
1913        }
1914        struct Input {
1915            attach: Endpoint,
1916            remote: Endpoint,
1917        }
1918        impl SerializableStruct for Input {
1919            fn schema(&self) -> &Schema<'_> {
1920                &INPUT_SCHEMA
1921            }
1922
1923            fn serialize_members(&self, s: &mut dyn ShapeSerializer) -> Result<(), SerdeError> {
1924                s.write_struct(&CC_ATTACH, &self.attach)?;
1925                s.write_struct(&CC_REMOTE, &self.remote)
1926            }
1927        }
1928
1929        // Scalar variant at top level.
1930        let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
1931        ser.write_struct(&ENDPOINT_SCHEMA, &Endpoint::Arn("dxcon-abc"))
1932            .unwrap();
1933        assert_eq!(
1934            String::from_utf8(ser.finish()).unwrap(),
1935            r#"{"arn":"dxcon-abc"}"#
1936        );
1937
1938        // Two union members: scalar + struct variant.
1939        let mut ser = JsonSerializer::new(Arc::new(JsonCodecSettings::default()));
1940        ser.write_struct(
1941            &INPUT_SCHEMA,
1942            &Input {
1943                attach: Endpoint::Arn("ap"),
1944                remote: Endpoint::Config(EndpointConfig { name: "ra" }),
1945            },
1946        )
1947        .unwrap();
1948        assert_eq!(
1949            String::from_utf8(ser.finish()).unwrap(),
1950            r#"{"attachPoint":{"arn":"ap"},"remoteAccount":{"config":{"name":"ra"}}}"#
1951        );
1952    }
1953}