Skip to main content

fastapi_openapi/
schema.rs

1//! JSON Schema types for OpenAPI 3.1.
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// JSON Schema representation.
7#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(untagged)]
9pub enum Schema {
10    /// Boolean schema (true = any, false = none).
11    Boolean(bool),
12    /// Reference to another schema.
13    Ref(RefSchema),
14    /// Object schema.
15    Object(ObjectSchema),
16    /// Array schema.
17    Array(ArraySchema),
18    /// Primitive type schema.
19    Primitive(PrimitiveSchema),
20    /// Enum schema (string values).
21    Enum(EnumSchema),
22    /// OneOf schema (union type).
23    OneOf(OneOfSchema),
24}
25
26impl Schema {
27    /// Create a string schema.
28    pub fn string() -> Self {
29        Schema::Primitive(PrimitiveSchema::string())
30    }
31
32    /// Create an integer schema with optional format.
33    pub fn integer(format: Option<&str>) -> Self {
34        Schema::Primitive(PrimitiveSchema::integer(format))
35    }
36
37    /// Create a number schema with optional format.
38    pub fn number(format: Option<&str>) -> Self {
39        Schema::Primitive(PrimitiveSchema::number(format))
40    }
41
42    /// Create a boolean schema.
43    pub fn boolean() -> Self {
44        Schema::Primitive(PrimitiveSchema::boolean())
45    }
46
47    /// Create a reference schema.
48    pub fn reference(name: &str) -> Self {
49        Schema::Ref(RefSchema {
50            reference: format!("#/components/schemas/{name}"),
51        })
52    }
53
54    /// Create an array schema.
55    pub fn array(items: Schema) -> Self {
56        Schema::Array(ArraySchema {
57            items: Box::new(items),
58            min_items: None,
59            max_items: None,
60        })
61    }
62
63    /// Create an object schema with the given properties.
64    pub fn object(properties: HashMap<String, Schema>, required: Vec<String>) -> Self {
65        Schema::Object(ObjectSchema {
66            title: None,
67            description: None,
68            properties,
69            required,
70            additional_properties: None,
71        })
72    }
73
74    /// Set nullable on this schema (if primitive).
75    #[must_use]
76    pub fn nullable(mut self) -> Self {
77        if let Schema::Primitive(ref mut p) = self {
78            p.nullable = true;
79        }
80        self
81    }
82
83    /// Set title on this schema (if object).
84    #[must_use]
85    pub fn with_title(mut self, title: impl Into<String>) -> Self {
86        if let Schema::Object(ref mut o) = self {
87            o.title = Some(title.into());
88        }
89        self
90    }
91
92    /// Set description on this schema (if object).
93    #[must_use]
94    pub fn with_description(mut self, description: impl Into<String>) -> Self {
95        if let Schema::Object(ref mut o) = self {
96            o.description = Some(description.into());
97        }
98        self
99    }
100
101    /// Create a string enum schema with allowed values.
102    pub fn string_enum(values: Vec<String>) -> Self {
103        Schema::Enum(EnumSchema {
104            schema_type: SchemaType::String,
105            enum_values: values,
106        })
107    }
108
109    /// Create a oneOf schema (union type).
110    pub fn one_of(schemas: Vec<Schema>) -> Self {
111        Schema::OneOf(OneOfSchema { one_of: schemas })
112    }
113}
114
115/// Schema reference.
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct RefSchema {
118    /// Reference path (e.g., "#/components/schemas/Item").
119    #[serde(rename = "$ref")]
120    pub reference: String,
121}
122
123/// Object schema.
124#[derive(Debug, Clone, Default, Serialize, Deserialize)]
125pub struct ObjectSchema {
126    /// Schema title.
127    #[serde(default, skip_serializing_if = "Option::is_none")]
128    pub title: Option<String>,
129    /// Schema description.
130    #[serde(default, skip_serializing_if = "Option::is_none")]
131    pub description: Option<String>,
132    /// Object properties.
133    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
134    pub properties: HashMap<String, Schema>,
135    /// Required property names.
136    #[serde(default, skip_serializing_if = "Vec::is_empty")]
137    pub required: Vec<String>,
138    /// Additional properties schema.
139    #[serde(default, skip_serializing_if = "Option::is_none")]
140    pub additional_properties: Option<Box<Schema>>,
141}
142
143/// Array schema.
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub struct ArraySchema {
146    /// Item schema.
147    pub items: Box<Schema>,
148    /// Minimum items.
149    #[serde(default, skip_serializing_if = "Option::is_none")]
150    pub min_items: Option<usize>,
151    /// Maximum items.
152    #[serde(default, skip_serializing_if = "Option::is_none")]
153    pub max_items: Option<usize>,
154}
155
156/// Enum schema with allowed values.
157#[derive(Debug, Clone, Serialize, Deserialize)]
158pub struct EnumSchema {
159    /// JSON Schema type (typically string for enums).
160    #[serde(rename = "type")]
161    pub schema_type: SchemaType,
162    /// Allowed enum values.
163    #[serde(rename = "enum")]
164    pub enum_values: Vec<String>,
165}
166
167/// OneOf schema (union type).
168#[derive(Debug, Clone, Serialize, Deserialize)]
169pub struct OneOfSchema {
170    /// List of possible schemas.
171    #[serde(rename = "oneOf")]
172    pub one_of: Vec<Schema>,
173}
174
175/// Primitive type schema.
176#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct PrimitiveSchema {
178    /// JSON Schema type.
179    #[serde(rename = "type")]
180    pub schema_type: SchemaType,
181    /// Format hint.
182    #[serde(default, skip_serializing_if = "Option::is_none")]
183    pub format: Option<String>,
184    /// Nullable flag (OpenAPI 3.1).
185    #[serde(default, skip_serializing_if = "is_false")]
186    pub nullable: bool,
187}
188
189impl PrimitiveSchema {
190    /// Create a string schema.
191    pub fn string() -> Self {
192        Self {
193            schema_type: SchemaType::String,
194            format: None,
195            nullable: false,
196        }
197    }
198
199    /// Create an integer schema with optional format.
200    pub fn integer(format: Option<&str>) -> Self {
201        Self {
202            schema_type: SchemaType::Integer,
203            format: format.map(String::from),
204            nullable: false,
205        }
206    }
207
208    /// Create a number schema with optional format.
209    pub fn number(format: Option<&str>) -> Self {
210        Self {
211            schema_type: SchemaType::Number,
212            format: format.map(String::from),
213            nullable: false,
214        }
215    }
216
217    /// Create a boolean schema.
218    pub fn boolean() -> Self {
219        Self {
220            schema_type: SchemaType::Boolean,
221            format: None,
222            nullable: false,
223        }
224    }
225}
226
227#[allow(clippy::trivially_copy_pass_by_ref)]
228fn is_false(b: &bool) -> bool {
229    !*b
230}
231
232/// JSON Schema primitive types.
233#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
234#[serde(rename_all = "lowercase")]
235pub enum SchemaType {
236    /// String type.
237    String,
238    /// Number type (float).
239    Number,
240    /// Integer type.
241    Integer,
242    /// Boolean type.
243    Boolean,
244    /// Null type.
245    Null,
246}
247
248/// Trait for types that can generate JSON Schema.
249pub trait JsonSchema {
250    /// Generate the JSON Schema for this type.
251    fn schema() -> Schema;
252
253    /// Get the schema name for use in `#/components/schemas/`.
254    #[must_use]
255    fn schema_name() -> Option<&'static str> {
256        None
257    }
258}
259
260// Implement for primitive types
261impl JsonSchema for String {
262    fn schema() -> Schema {
263        Schema::Primitive(PrimitiveSchema {
264            schema_type: SchemaType::String,
265            format: None,
266            nullable: false,
267        })
268    }
269}
270
271impl JsonSchema for i64 {
272    fn schema() -> Schema {
273        Schema::Primitive(PrimitiveSchema {
274            schema_type: SchemaType::Integer,
275            format: Some("int64".to_string()),
276            nullable: false,
277        })
278    }
279}
280
281impl JsonSchema for i32 {
282    fn schema() -> Schema {
283        Schema::Primitive(PrimitiveSchema {
284            schema_type: SchemaType::Integer,
285            format: Some("int32".to_string()),
286            nullable: false,
287        })
288    }
289}
290
291impl JsonSchema for f64 {
292    fn schema() -> Schema {
293        Schema::Primitive(PrimitiveSchema {
294            schema_type: SchemaType::Number,
295            format: Some("double".to_string()),
296            nullable: false,
297        })
298    }
299}
300
301impl JsonSchema for bool {
302    fn schema() -> Schema {
303        Schema::Primitive(PrimitiveSchema {
304            schema_type: SchemaType::Boolean,
305            format: None,
306            nullable: false,
307        })
308    }
309}
310
311impl<T: JsonSchema> JsonSchema for Option<T> {
312    fn schema() -> Schema {
313        match T::schema() {
314            Schema::Primitive(mut p) => {
315                p.nullable = true;
316                Schema::Primitive(p)
317            }
318            other => other,
319        }
320    }
321}
322
323impl<T: JsonSchema> JsonSchema for Vec<T> {
324    fn schema() -> Schema {
325        Schema::Array(ArraySchema {
326            items: Box::new(T::schema()),
327            min_items: None,
328            max_items: None,
329        })
330    }
331}