specta_serde/
lib.rs

1//! Serde support for Specta
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![doc(
4    html_logo_url = "https://github.com/oscartbeaumont/specta/raw/main/.github/logo-128.png",
5    html_favicon_url = "https://github.com/oscartbeaumont/specta/raw/main/.github/logo-128.png"
6)]
7
8use std::collections::HashSet;
9
10use thiserror::Error;
11
12use specta::{
13    datatype::{
14        DataType, EnumRepr, EnumType, EnumVariants, LiteralType, PrimitiveType, StructFields,
15    },
16    internal::{resolve_generics, skip_fields, skip_fields_named},
17    SpectaID, TypeCollection,
18};
19
20// TODO: The error should show a path to the type causing the issue like the BigInt error reporting.
21
22#[derive(Error, Debug, PartialEq)]
23pub enum SerdeError {
24    #[error("A map key must be a 'string' or 'number' type")]
25    InvalidMapKey,
26    #[error("#[specta(tag = \"...\")] cannot be used with tuple variants")]
27    InvalidInternallyTaggedEnum,
28    #[error("the usage of #[specta(skip)] means the type can't be serialized")]
29    InvalidUsageOfSkip,
30}
31
32/// Check that a [DataType] is a valid for Serde.
33///
34/// This can be used by exporters which wanna do export-time checks that all types are compatible with Serde formats.
35pub fn is_valid_ty(dt: &DataType, type_map: &TypeCollection) -> Result<(), SerdeError> {
36    is_valid_ty_internal(dt, type_map, &mut Default::default())
37}
38
39fn is_valid_ty_internal(
40    dt: &DataType,
41    type_map: &TypeCollection,
42    checked_references: &mut HashSet<SpectaID>,
43) -> Result<(), SerdeError> {
44    match dt {
45        DataType::Nullable(ty) => is_valid_ty(ty, type_map)?,
46        DataType::Map(ty) => {
47            is_valid_map_key(ty.key_ty(), type_map)?;
48            is_valid_ty_internal(ty.value_ty(), type_map, checked_references)?;
49        }
50        DataType::Struct(ty) => match ty.fields() {
51            StructFields::Unit => {}
52            StructFields::Unnamed(ty) => {
53                for (_, ty) in skip_fields(ty.fields()) {
54                    is_valid_ty_internal(ty, type_map, checked_references)?;
55                }
56            }
57            StructFields::Named(ty) => {
58                for (_, (_, ty)) in skip_fields_named(ty.fields()) {
59                    is_valid_ty_internal(ty, type_map, checked_references)?;
60                }
61            }
62        },
63        DataType::Enum(ty) => {
64            validate_enum(ty, type_map)?;
65
66            for (_variant_name, variant) in ty.variants().iter() {
67                match &variant.inner() {
68                    EnumVariants::Unit => {}
69                    EnumVariants::Named(variant) => {
70                        for (_, (_, ty)) in skip_fields_named(variant.fields()) {
71                            is_valid_ty_internal(ty, type_map, checked_references)?;
72                        }
73                    }
74                    EnumVariants::Unnamed(variant) => {
75                        for (_, ty) in skip_fields(variant.fields()) {
76                            is_valid_ty_internal(ty, type_map, checked_references)?;
77                        }
78                    }
79                }
80            }
81        }
82        DataType::Tuple(ty) => {
83            for ty in ty.elements() {
84                is_valid_ty_internal(ty, type_map, checked_references)?;
85            }
86        }
87        DataType::Reference(ty) => {
88            for (_, generic) in ty.generics() {
89                is_valid_ty_internal(generic, type_map, checked_references)?;
90            }
91
92            #[allow(clippy::panic)]
93            if !checked_references.contains(&ty.sid()) {
94                checked_references.insert(ty.sid());
95                let ty = type_map.get(ty.sid()).unwrap_or_else(|| {
96                    panic!("Type '{}' was never populated.", ty.sid().type_name())
97                }); // TODO: Error properly
98
99                is_valid_ty_internal(&ty.inner, type_map, checked_references)?;
100            }
101        }
102        _ => {}
103    }
104
105    Ok(())
106}
107
108// Typescript: Must be assignable to `string | number | symbol` says Typescript.
109fn is_valid_map_key(key_ty: &DataType, type_map: &TypeCollection) -> Result<(), SerdeError> {
110    match key_ty {
111        DataType::Any => Ok(()),
112        DataType::Primitive(ty) => match ty {
113            PrimitiveType::i8
114            | PrimitiveType::i16
115            | PrimitiveType::i32
116            | PrimitiveType::i64
117            | PrimitiveType::i128
118            | PrimitiveType::isize
119            | PrimitiveType::u8
120            | PrimitiveType::u16
121            | PrimitiveType::u32
122            | PrimitiveType::u64
123            | PrimitiveType::u128
124            | PrimitiveType::usize
125            | PrimitiveType::f32
126            | PrimitiveType::f64
127            | PrimitiveType::String
128            | PrimitiveType::char => Ok(()),
129            _ => Err(SerdeError::InvalidMapKey),
130        },
131        DataType::Literal(ty) => match ty {
132            LiteralType::i8(_)
133            | LiteralType::i16(_)
134            | LiteralType::i32(_)
135            | LiteralType::u8(_)
136            | LiteralType::u16(_)
137            | LiteralType::u32(_)
138            | LiteralType::f32(_)
139            | LiteralType::f64(_)
140            | LiteralType::String(_)
141            | LiteralType::char(_) => Ok(()),
142            _ => Err(SerdeError::InvalidMapKey),
143        },
144        // Enum of other valid types are also valid Eg. `"A" | "B"` or `"A" | 5` are valid
145        DataType::Enum(ty) => {
146            for (_variant_name, variant) in ty.variants() {
147                match &variant.inner() {
148                    EnumVariants::Unit => {}
149                    EnumVariants::Unnamed(item) => {
150                        if item.fields().len() > 1 {
151                            return Err(SerdeError::InvalidMapKey);
152                        }
153
154                        if *ty.repr() != EnumRepr::Untagged {
155                            return Err(SerdeError::InvalidMapKey);
156                        }
157                    }
158                    _ => return Err(SerdeError::InvalidMapKey),
159                }
160            }
161
162            Ok(())
163        }
164        DataType::Reference(r) => {
165            let ty = type_map.get(r.sid()).expect("Type was never populated"); // TODO: Error properly
166
167            is_valid_map_key(&resolve_generics(ty.inner.clone(), r.generics()), type_map)
168        }
169        _ => Err(SerdeError::InvalidMapKey),
170    }
171}
172
173// Serde does not allow serializing a variant of certain types of enum's.
174fn validate_enum(e: &EnumType, type_map: &TypeCollection) -> Result<(), SerdeError> {
175    // You can't `#[serde(skip)]` your way to an empty enum.
176    let valid_variants = e.variants().iter().filter(|(_, v)| !v.skip()).count();
177    if valid_variants == 0 && !e.variants().is_empty() {
178        return Err(SerdeError::InvalidUsageOfSkip);
179    }
180
181    // Only internally tagged enums can be invalid.
182    if let EnumRepr::Internal { .. } = e.repr() {
183        validate_internally_tag_enum(e, type_map)?;
184    }
185
186    Ok(())
187}
188
189// Checks for specially internally tagged enums.
190fn validate_internally_tag_enum(e: &EnumType, type_map: &TypeCollection) -> Result<(), SerdeError> {
191    for (_variant_name, variant) in e.variants() {
192        match &variant.inner() {
193            EnumVariants::Unit => {}
194            EnumVariants::Named(_) => {}
195            EnumVariants::Unnamed(item) => {
196                let mut fields = skip_fields(item.fields());
197
198                let Some(first_field) = fields.next() else {
199                    continue;
200                };
201
202                if fields.next().is_some() {
203                    return Err(SerdeError::InvalidInternallyTaggedEnum);
204                }
205
206                validate_internally_tag_enum_datatype(first_field.1, type_map)?;
207            }
208        }
209    }
210
211    Ok(())
212}
213
214// Internally tagged enums require map-type's (with a couple of exceptions like `null`)
215// Which makes sense when you can't represent `{ "type": "A" } & string` in a single JSON value.
216fn validate_internally_tag_enum_datatype(
217    ty: &DataType,
218    type_map: &TypeCollection,
219) -> Result<(), SerdeError> {
220    match ty {
221        // `serde_json::Any` can be *technically* be either valid or invalid based on the actual data but we are being strict and reject it.
222        DataType::Any => return Err(SerdeError::InvalidInternallyTaggedEnum),
223        DataType::Map(_) => {}
224        // Structs's are always map-types unless they are transparent then it depends on inner type. However, transparent passes through when calling `Type::inline` so we don't need to specially check that case.
225        DataType::Struct(_) => {}
226        DataType::Enum(ty) => match ty.repr() {
227            // Is only valid if the enum itself is also valid.
228            EnumRepr::Untagged => validate_internally_tag_enum(ty, type_map)?,
229            // Eg. `{ "Variant": "value" }` is a map-type so valid.
230            EnumRepr::External => {}
231            // Eg. `{ "type": "variant", "field": "value" }` is a map-type so valid.
232            EnumRepr::Internal { .. } => {}
233            // Eg. `{ "type": "variant", "c": {} }` is a map-type so valid.
234            EnumRepr::Adjacent { .. } => {}
235        },
236        // `()` is `null` and is valid
237        DataType::Tuple(ty) if ty.elements().is_empty() => {}
238        // References need to be checked against the same rules.
239        DataType::Reference(ty) => {
240            let ty = type_map.get(ty.sid()).expect("Type was never populated"); // TODO: Error properly
241
242            validate_internally_tag_enum_datatype(&ty.inner, type_map)?;
243        }
244        _ => return Err(SerdeError::InvalidInternallyTaggedEnum),
245    }
246
247    Ok(())
248}