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::ShapeSerializer;
9use aws_smithy_schema::{Schema, ShapeId};
10use aws_smithy_types::date_time::Format as TimestampFormat;
11use aws_smithy_types::{BigDecimal, BigInteger, Blob, DateTime, Document};
12use std::fmt;
13
14use crate::codec::JsonCodecSettings;
15
16/// Error type for JSON serialization.
17#[derive(Debug)]
18pub enum JsonSerializerError {
19    /// An error occurred during JSON writing.
20    WriteError(String),
21}
22
23impl fmt::Display for JsonSerializerError {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        match self {
26            Self::WriteError(msg) => write!(f, "JSON write error: {}", msg),
27        }
28    }
29}
30
31impl std::error::Error for JsonSerializerError {}
32
33/// JSON serializer that implements the ShapeSerializer trait.
34pub struct JsonSerializer {
35    output: String,
36    settings: JsonCodecSettings,
37}
38
39impl JsonSerializer {
40    /// Creates a new JSON serializer with the given settings.
41    pub fn new(settings: JsonCodecSettings) -> Self {
42        Self {
43            output: String::new(),
44            settings,
45        }
46    }
47
48    /// Gets the timestamp format to use, respecting @timestampFormat trait.
49    fn get_timestamp_format(&self, schema: &dyn Schema) -> TimestampFormat {
50        let timestamp_format_trait_id = ShapeId::new("smithy.api#timestampFormat");
51        if let Some(trait_obj) = schema.traits().get(&timestamp_format_trait_id) {
52            if let Some(format_str) = trait_obj.as_any().downcast_ref::<String>() {
53                return match format_str.as_str() {
54                    "epoch-seconds" => TimestampFormat::EpochSeconds,
55                    "http-date" => TimestampFormat::HttpDate,
56                    "date-time" => TimestampFormat::DateTime,
57                    _ => self.settings.default_timestamp_format,
58                };
59            }
60        }
61        self.settings.default_timestamp_format
62    }
63
64    fn write_json_value(&mut self, doc: &Document) {
65        use crate::serialize::JsonValueWriter;
66        let writer = JsonValueWriter::new(&mut self.output);
67        writer.document(doc);
68    }
69}
70
71impl ShapeSerializer for JsonSerializer {
72    type Output = Vec<u8>;
73    type Error = JsonSerializerError;
74
75    fn finish(self) -> Result<Self::Output, Self::Error> {
76        Ok(self.output.into_bytes())
77    }
78
79    fn write_struct<F>(&mut self, _schema: &dyn Schema, write_members: F) -> Result<(), Self::Error>
80    where
81        F: FnOnce(&mut Self) -> Result<(), Self::Error>,
82    {
83        self.output.push('{');
84        write_members(self)?;
85        self.output.push('}');
86        Ok(())
87    }
88
89    fn write_list<F>(&mut self, _schema: &dyn Schema, write_elements: F) -> Result<(), Self::Error>
90    where
91        F: FnOnce(&mut Self) -> Result<(), Self::Error>,
92    {
93        self.output.push('[');
94        write_elements(self)?;
95        self.output.push(']');
96        Ok(())
97    }
98
99    fn write_map<F>(&mut self, _schema: &dyn Schema, write_entries: F) -> Result<(), Self::Error>
100    where
101        F: FnOnce(&mut Self) -> Result<(), Self::Error>,
102    {
103        self.output.push('{');
104        write_entries(self)?;
105        self.output.push('}');
106        Ok(())
107    }
108
109    fn write_boolean(&mut self, _schema: &dyn Schema, value: bool) -> Result<(), Self::Error> {
110        self.output.push_str(if value { "true" } else { "false" });
111        Ok(())
112    }
113
114    fn write_byte(&mut self, _schema: &dyn Schema, value: i8) -> Result<(), Self::Error> {
115        use std::fmt::Write;
116        write!(&mut self.output, "{}", value)
117            .map_err(|e| JsonSerializerError::WriteError(e.to_string()))
118    }
119
120    fn write_short(&mut self, _schema: &dyn Schema, value: i16) -> Result<(), Self::Error> {
121        use std::fmt::Write;
122        write!(&mut self.output, "{}", value)
123            .map_err(|e| JsonSerializerError::WriteError(e.to_string()))
124    }
125
126    fn write_integer(&mut self, _schema: &dyn Schema, value: i32) -> Result<(), Self::Error> {
127        use std::fmt::Write;
128        write!(&mut self.output, "{}", value)
129            .map_err(|e| JsonSerializerError::WriteError(e.to_string()))
130    }
131
132    fn write_long(&mut self, _schema: &dyn Schema, value: i64) -> Result<(), Self::Error> {
133        use std::fmt::Write;
134        write!(&mut self.output, "{}", value)
135            .map_err(|e| JsonSerializerError::WriteError(e.to_string()))
136    }
137
138    fn write_float(&mut self, _schema: &dyn Schema, value: f32) -> Result<(), Self::Error> {
139        use std::fmt::Write;
140        write!(&mut self.output, "{}", value)
141            .map_err(|e| JsonSerializerError::WriteError(e.to_string()))
142    }
143
144    fn write_double(&mut self, _schema: &dyn Schema, value: f64) -> Result<(), Self::Error> {
145        use std::fmt::Write;
146        write!(&mut self.output, "{}", value)
147            .map_err(|e| JsonSerializerError::WriteError(e.to_string()))
148    }
149
150    fn write_big_integer(
151        &mut self,
152        _schema: &dyn Schema,
153        value: &BigInteger,
154    ) -> Result<(), Self::Error> {
155        self.output.push_str(value.as_ref());
156        Ok(())
157    }
158
159    fn write_big_decimal(
160        &mut self,
161        _schema: &dyn Schema,
162        value: &BigDecimal,
163    ) -> Result<(), Self::Error> {
164        self.output.push_str(value.as_ref());
165        Ok(())
166    }
167
168    fn write_string(&mut self, _schema: &dyn Schema, value: &str) -> Result<(), Self::Error> {
169        use crate::escape::escape_string;
170        self.output.push('"');
171        self.output.push_str(&escape_string(value));
172        self.output.push('"');
173        Ok(())
174    }
175
176    fn write_blob(&mut self, _schema: &dyn Schema, value: &Blob) -> Result<(), Self::Error> {
177        use aws_smithy_types::base64;
178        let encoded = base64::encode(value.as_ref());
179        self.output.push('"');
180        self.output.push_str(&encoded);
181        self.output.push('"');
182        Ok(())
183    }
184
185    fn write_timestamp(
186        &mut self,
187        schema: &dyn Schema,
188        value: &DateTime,
189    ) -> Result<(), Self::Error> {
190        let format = self.get_timestamp_format(schema);
191        let formatted = value.fmt(format).map_err(|e| {
192            JsonSerializerError::WriteError(format!("Failed to format timestamp: {}", e))
193        })?;
194
195        match format {
196            TimestampFormat::EpochSeconds => {
197                // Epoch seconds as number
198                self.output.push_str(&formatted);
199            }
200            _ => {
201                // Other formats as strings
202                self.output.push('"');
203                self.output.push_str(&formatted);
204                self.output.push('"');
205            }
206        }
207        Ok(())
208    }
209
210    fn write_document(
211        &mut self,
212        _schema: &dyn Schema,
213        value: &Document,
214    ) -> Result<(), Self::Error> {
215        self.write_json_value(value);
216        Ok(())
217    }
218
219    fn write_null(&mut self, _schema: &dyn Schema) -> Result<(), Self::Error> {
220        self.output.push_str("null");
221        Ok(())
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228    use aws_smithy_schema::prelude::*;
229
230    #[test]
231    fn test_write_boolean() {
232        let mut ser = JsonSerializer::new(JsonCodecSettings::default());
233        ser.write_boolean(&BOOLEAN, true).unwrap();
234        let output = ser.finish().unwrap();
235        assert_eq!(String::from_utf8(output).unwrap(), "true");
236    }
237
238    #[test]
239    fn test_write_string() {
240        let mut ser = JsonSerializer::new(JsonCodecSettings::default());
241        ser.write_string(&STRING, "hello").unwrap();
242        let output = ser.finish().unwrap();
243        assert_eq!(String::from_utf8(output).unwrap(), "\"hello\"");
244    }
245
246    #[test]
247    fn test_write_integer() {
248        let mut ser = JsonSerializer::new(JsonCodecSettings::default());
249        ser.write_integer(&INTEGER, 42).unwrap();
250        let output = ser.finish().unwrap();
251        assert_eq!(String::from_utf8(output).unwrap(), "42");
252    }
253
254    #[test]
255    fn test_write_null() {
256        let mut ser = JsonSerializer::new(JsonCodecSettings::default());
257        ser.write_null(&STRING).unwrap();
258        let output = ser.finish().unwrap();
259        assert_eq!(String::from_utf8(output).unwrap(), "null");
260    }
261
262    #[test]
263    fn test_write_list() {
264        let mut ser = JsonSerializer::new(JsonCodecSettings::default());
265        // Create a simple list schema for testing
266        let list_schema = aws_smithy_schema::prelude::PreludeSchema::new(
267            aws_smithy_schema::ShapeId::new("test#List"),
268            aws_smithy_schema::ShapeType::List,
269        );
270        ser.write_list(&list_schema, |s| {
271            s.write_integer(&INTEGER, 1)?;
272            s.output.push(',');
273            s.write_integer(&INTEGER, 2)?;
274            s.output.push(',');
275            s.write_integer(&INTEGER, 3)?;
276            Ok(())
277        })
278        .unwrap();
279        let output = ser.finish().unwrap();
280        assert_eq!(String::from_utf8(output).unwrap(), "[1,2,3]");
281    }
282
283    #[test]
284    fn test_write_full_object() {
285        let mut ser = JsonSerializer::new(JsonCodecSettings::default());
286        let struct_schema = aws_smithy_schema::prelude::PreludeSchema::new(
287            aws_smithy_schema::ShapeId::new("test#Struct"),
288            aws_smithy_schema::ShapeType::Structure,
289        );
290        let list_schema = aws_smithy_schema::prelude::PreludeSchema::new(
291            aws_smithy_schema::ShapeId::new("test#List"),
292            aws_smithy_schema::ShapeType::List,
293        );
294        ser.write_struct(&struct_schema, |s| {
295            s.output.push_str("\"active\":");
296            s.write_boolean(&BOOLEAN, true)?;
297            s.output.push(',');
298            s.output.push_str("\"name\":");
299            s.write_string(&STRING, "test")?;
300            s.output.push(',');
301            s.output.push_str("\"count\":");
302            s.write_integer(&INTEGER, 42)?;
303            s.output.push(',');
304            s.output.push_str("\"price\":");
305            s.write_float(&FLOAT, 3.14)?;
306            s.output.push(',');
307            s.output.push_str("\"items\":");
308            s.write_list(&list_schema, |ls| {
309                ls.write_integer(&INTEGER, 1)?;
310                ls.output.push(',');
311                ls.write_integer(&INTEGER, 2)?;
312                Ok(())
313            })?;
314            Ok(())
315        })
316        .unwrap();
317        let output = ser.finish().unwrap();
318        assert_eq!(
319            String::from_utf8(output).unwrap(),
320            "{\"active\":true,\"name\":\"test\",\"count\":42,\"price\":3.14,\"items\":[1,2]}"
321        );
322    }
323
324    #[test]
325    fn test_nested_complex_serialization() {
326        let mut ser = JsonSerializer::new(JsonCodecSettings::default());
327        let struct_schema = aws_smithy_schema::prelude::PreludeSchema::new(
328            aws_smithy_schema::ShapeId::new("test#User"),
329            aws_smithy_schema::ShapeType::Structure,
330        );
331        let list_schema = aws_smithy_schema::prelude::PreludeSchema::new(
332            aws_smithy_schema::ShapeId::new("test#List"),
333            aws_smithy_schema::ShapeType::List,
334        );
335        let map_schema = aws_smithy_schema::prelude::PreludeSchema::new(
336            aws_smithy_schema::ShapeId::new("test#Map"),
337            aws_smithy_schema::ShapeType::Map,
338        );
339
340        ser.write_struct(&struct_schema, |s| {
341            s.output.push_str("\"id\":");
342            s.write_long(&LONG, 12345)?;
343            s.output.push(',');
344            s.output.push_str("\"name\":");
345            s.write_string(&STRING, "John Doe")?;
346            s.output.push(',');
347            s.output.push_str("\"scores\":");
348            s.write_list(&list_schema, |ls| {
349                ls.write_double(&DOUBLE, 95.5)?;
350                ls.output.push(',');
351                ls.write_double(&DOUBLE, 87.3)?;
352                ls.output.push(',');
353                ls.write_double(&DOUBLE, 92.1)?;
354                Ok(())
355            })?;
356            s.output.push(',');
357            s.output.push_str("\"address\":");
358            s.write_struct(&struct_schema, |addr| {
359                addr.output.push_str("\"street\":");
360                addr.write_string(&STRING, "123 Main St")?;
361                addr.output.push(',');
362                addr.output.push_str("\"city\":");
363                addr.write_string(&STRING, "Seattle")?;
364                addr.output.push(',');
365                addr.output.push_str("\"zip\":");
366                addr.write_integer(&INTEGER, 98101)?;
367                Ok(())
368            })?;
369            s.output.push(',');
370            s.output.push_str("\"companies\":");
371            s.write_list(&list_schema, |ls| {
372                ls.write_struct(&struct_schema, |comp| {
373                    comp.output.push_str("\"name\":");
374                    comp.write_string(&STRING, "TechCorp")?;
375                    comp.output.push(',');
376                    comp.output.push_str("\"employees\":");
377                    comp.write_list(&list_schema, |emp| {
378                        emp.write_string(&STRING, "Alice")?;
379                        emp.output.push(',');
380                        emp.write_string(&STRING, "Bob")?;
381                        Ok(())
382                    })?;
383                    comp.output.push(',');
384                    comp.output.push_str("\"metadata\":");
385                    comp.write_map(&map_schema, |meta| {
386                        meta.output.push_str("\"founded\":");
387                        meta.write_integer(&INTEGER, 2010)?;
388                        meta.output.push(',');
389                        meta.output.push_str("\"size\":");
390                        meta.write_integer(&INTEGER, 500)?;
391                        Ok(())
392                    })?;
393                    comp.output.push(',');
394                    comp.output.push_str("\"active\":");
395                    comp.write_boolean(&BOOLEAN, true)?;
396                    Ok(())
397                })?;
398                ls.output.push(',');
399                ls.write_struct(&struct_schema, |comp| {
400                    comp.output.push_str("\"name\":");
401                    comp.write_string(&STRING, "StartupInc")?;
402                    comp.output.push(',');
403                    comp.output.push_str("\"employees\":");
404                    comp.write_list(&list_schema, |emp| {
405                        emp.write_string(&STRING, "Charlie")?;
406                        Ok(())
407                    })?;
408                    comp.output.push(',');
409                    comp.output.push_str("\"metadata\":");
410                    comp.write_map(&map_schema, |meta| {
411                        meta.output.push_str("\"founded\":");
412                        meta.write_integer(&INTEGER, 2020)?;
413                        Ok(())
414                    })?;
415                    comp.output.push(',');
416                    comp.output.push_str("\"active\":");
417                    comp.write_boolean(&BOOLEAN, false)?;
418                    Ok(())
419                })?;
420                Ok(())
421            })?;
422            s.output.push(',');
423            s.output.push_str("\"tags\":");
424            s.write_map(&map_schema, |tags| {
425                tags.output.push_str("\"role\":");
426                tags.write_string(&STRING, "admin")?;
427                tags.output.push(',');
428                tags.output.push_str("\"level\":");
429                tags.write_string(&STRING, "senior")?;
430                Ok(())
431            })?;
432            Ok(())
433        })
434        .unwrap();
435
436        let output = String::from_utf8(ser.finish().unwrap()).unwrap();
437        let expected = r#"{"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"}}"#;
438        assert_eq!(output, expected);
439    }
440}