aws_smithy_json/
codec.rs

1/*
2 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3 * SPDX-License-Identifier: Apache-2.0
4 */
5
6//! JSON codec implementation for schema-based serialization.
7
8use aws_smithy_schema::codec::Codec;
9use aws_smithy_schema::Schema;
10use aws_smithy_types::date_time::Format as TimestampFormat;
11use std::sync::Arc;
12
13mod deserializer;
14mod serializer;
15
16pub use deserializer::JsonDeserializer;
17pub use serializer::JsonSerializer;
18
19/// Maps between Smithy member names and JSON wire field names.
20///
21/// When `@jsonName` is enabled, the wire name may differ from the member name.
22/// This type handles the mapping in both directions and caches the reverse
23/// lookup (wire name → member index) per struct schema.
24#[derive(Debug)]
25enum JsonFieldMapper {
26    /// Uses member names directly, ignoring `@jsonName`.
27    UseMemberName,
28    /// Uses `@jsonName` trait values when present, falling back to member name.
29    UseJsonName,
30}
31
32impl JsonFieldMapper {
33    /// Returns the JSON wire name for a member schema.
34    fn member_to_field<'a>(&self, member: &'a Schema) -> Option<&'a str> {
35        let name = member.member_name()?;
36        match self {
37            JsonFieldMapper::UseMemberName => Some(name),
38            JsonFieldMapper::UseJsonName => {
39                if let Some(jn) = member.json_name() {
40                    return Some(jn.value());
41                }
42                Some(name)
43            }
44        }
45    }
46
47    /// Resolves a JSON wire field name to a member schema within a struct schema.
48    fn field_to_member<'s>(&self, schema: &'s Schema, field_name: &str) -> Option<&'s Schema> {
49        match self {
50            JsonFieldMapper::UseMemberName => schema.member_schema(field_name),
51            JsonFieldMapper::UseJsonName => {
52                // Check @jsonName on each member. For typical struct sizes
53                // (< 50 members), linear scan is faster than a cached HashMap
54                // behind a Mutex.
55                for member in schema.members() {
56                    if let Some(jn) = member.json_name() {
57                        if jn.value() == field_name {
58                            return Some(member);
59                        }
60                    } else if member.member_name() == Some(field_name) {
61                        return Some(member);
62                    }
63                }
64                None
65            }
66        }
67    }
68}
69
70/// Configuration for JSON codec behavior.
71///
72/// Use the builder methods to construct settings:
73/// ```
74/// use aws_smithy_json::codec::JsonCodecSettings;
75///
76/// let settings = JsonCodecSettings::builder()
77///     .use_json_name(false)
78///     .build();
79/// ```
80#[derive(Debug)]
81pub struct JsonCodecSettings {
82    field_mapper: JsonFieldMapper,
83    default_timestamp_format: TimestampFormat,
84}
85
86impl JsonCodecSettings {
87    /// Creates a builder for `JsonCodecSettings`.
88    pub fn builder() -> JsonCodecSettingsBuilder {
89        JsonCodecSettingsBuilder::default()
90    }
91
92    /// Default timestamp format when not specified by `@timestampFormat` trait.
93    pub fn default_timestamp_format(&self) -> TimestampFormat {
94        self.default_timestamp_format
95    }
96
97    /// Returns the JSON wire name for a member schema.
98    pub(crate) fn member_to_field<'a>(&self, member: &'a Schema) -> Option<&'a str> {
99        self.field_mapper.member_to_field(member)
100    }
101
102    /// Resolves a JSON wire field name to a member schema.
103    pub(crate) fn field_to_member<'s>(
104        &self,
105        schema: &'s Schema,
106        field_name: &str,
107    ) -> Option<&'s Schema> {
108        self.field_mapper.field_to_member(schema, field_name)
109    }
110}
111
112impl Default for JsonCodecSettings {
113    fn default() -> Self {
114        Self {
115            field_mapper: JsonFieldMapper::UseJsonName,
116            default_timestamp_format: TimestampFormat::EpochSeconds,
117        }
118    }
119}
120
121/// Builder for [`JsonCodecSettings`].
122#[derive(Debug, Clone)]
123pub struct JsonCodecSettingsBuilder {
124    use_json_name: bool,
125    default_timestamp_format: TimestampFormat,
126}
127
128impl Default for JsonCodecSettingsBuilder {
129    fn default() -> Self {
130        Self {
131            use_json_name: true,
132            default_timestamp_format: TimestampFormat::EpochSeconds,
133        }
134    }
135}
136
137impl JsonCodecSettingsBuilder {
138    /// Whether to use the `@jsonName` trait for member names.
139    pub fn use_json_name(mut self, value: bool) -> Self {
140        self.use_json_name = value;
141        self
142    }
143
144    /// Default timestamp format when not specified by `@timestampFormat` trait.
145    pub fn default_timestamp_format(mut self, value: TimestampFormat) -> Self {
146        self.default_timestamp_format = value;
147        self
148    }
149
150    /// Builds the settings.
151    pub fn build(self) -> JsonCodecSettings {
152        let field_mapper = if self.use_json_name {
153            JsonFieldMapper::UseJsonName
154        } else {
155            JsonFieldMapper::UseMemberName
156        };
157        JsonCodecSettings {
158            field_mapper,
159            default_timestamp_format: self.default_timestamp_format,
160        }
161    }
162}
163
164/// JSON codec for schema-based serialization and deserialization.
165///
166/// # Examples
167///
168/// ```
169/// use aws_smithy_json::codec::{JsonCodec, JsonCodecSettings};
170/// use aws_smithy_schema::codec::Codec;
171///
172/// // Create codec with default settings (REST JSON style)
173/// let codec = JsonCodec::new(JsonCodecSettings::default());
174///
175/// // Create codec for AWS JSON RPC (no jsonName, epoch-seconds timestamps)
176/// let codec = JsonCodec::new(
177///     JsonCodecSettings::builder()
178///         .use_json_name(false)
179///         .build()
180/// );
181/// ```
182#[derive(Debug)]
183pub struct JsonCodec {
184    settings: Arc<JsonCodecSettings>,
185}
186
187impl JsonCodec {
188    /// Creates a new JSON codec with the given settings.
189    pub fn new(settings: JsonCodecSettings) -> Self {
190        Self {
191            settings: Arc::new(settings),
192        }
193    }
194
195    /// Returns the codec settings.
196    pub fn settings(&self) -> &JsonCodecSettings {
197        &self.settings
198    }
199}
200
201impl Default for JsonCodec {
202    fn default() -> Self {
203        Self::new(JsonCodecSettings::default())
204    }
205}
206
207impl Codec for JsonCodec {
208    type Serializer = JsonSerializer;
209    type Deserializer<'a> = JsonDeserializer<'a>;
210
211    fn create_serializer(&self) -> Self::Serializer {
212        JsonSerializer::new(self.settings.clone())
213    }
214
215    fn create_deserializer<'a>(&self, input: &'a [u8]) -> Self::Deserializer<'a> {
216        JsonDeserializer::new(input, self.settings.clone())
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn test_default_settings() {
226        let settings = JsonCodecSettings::default();
227        assert_eq!(
228            settings.default_timestamp_format(),
229            TimestampFormat::EpochSeconds
230        );
231    }
232
233    #[test]
234    fn test_builder() {
235        let settings = JsonCodecSettings::builder()
236            .use_json_name(false)
237            .default_timestamp_format(TimestampFormat::DateTime)
238            .build();
239        assert_eq!(
240            settings.default_timestamp_format(),
241            TimestampFormat::DateTime
242        );
243    }
244
245    #[test]
246    fn test_codec_creation() {
247        let codec = JsonCodec::default();
248        let _serializer = codec.create_serializer();
249        let _deserializer = codec.create_deserializer(b"{}");
250    }
251}