1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
14#[serde(untagged)]
15pub enum TypeArray {
16 Single(String),
18 Array(Vec<String>),
20}
21
22impl TypeArray {
23 pub fn single(ty: impl Into<String>) -> Self {
25 Self::Single(ty.into())
26 }
27
28 pub fn nullable(ty: impl Into<String>) -> Self {
30 Self::Array(vec![ty.into(), "null".to_string()])
31 }
32
33 pub fn array(types: Vec<String>) -> Self {
35 if types.len() == 1 {
36 Self::Single(types.into_iter().next().unwrap())
37 } else {
38 Self::Array(types)
39 }
40 }
41
42 pub fn is_nullable(&self) -> bool {
44 match self {
45 Self::Single(_) => false,
46 Self::Array(types) => types.iter().any(|t| t == "null"),
47 }
48 }
49
50 pub fn make_nullable(self) -> Self {
52 match self {
53 Self::Single(ty) => Self::Array(vec![ty, "null".to_string()]),
54 Self::Array(mut types) => {
55 if !types.iter().any(|t| t == "null") {
56 types.push("null".to_string());
57 }
58 Self::Array(types)
59 }
60 }
61 }
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
66#[serde(rename_all = "camelCase")]
67pub struct JsonSchema2020 {
68 #[serde(rename = "$schema", skip_serializing_if = "Option::is_none")]
70 pub schema: Option<String>,
71
72 #[serde(rename = "$id", skip_serializing_if = "Option::is_none")]
74 pub id: Option<String>,
75
76 #[serde(rename = "$ref", skip_serializing_if = "Option::is_none")]
78 pub reference: Option<String>,
79
80 #[serde(rename = "$dynamicRef", skip_serializing_if = "Option::is_none")]
82 pub dynamic_ref: Option<String>,
83
84 #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
86 pub schema_type: Option<TypeArray>,
87
88 #[serde(skip_serializing_if = "Option::is_none")]
90 pub title: Option<String>,
91
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub description: Option<String>,
95
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub default: Option<serde_json::Value>,
99
100 #[serde(rename = "const", skip_serializing_if = "Option::is_none")]
102 pub const_value: Option<serde_json::Value>,
103
104 #[serde(rename = "enum", skip_serializing_if = "Option::is_none")]
106 pub enum_values: Option<Vec<serde_json::Value>>,
107
108 #[serde(skip_serializing_if = "Option::is_none")]
111 pub min_length: Option<u64>,
112
113 #[serde(skip_serializing_if = "Option::is_none")]
115 pub max_length: Option<u64>,
116
117 #[serde(skip_serializing_if = "Option::is_none")]
119 pub pattern: Option<String>,
120
121 #[serde(skip_serializing_if = "Option::is_none")]
123 pub format: Option<String>,
124
125 #[serde(skip_serializing_if = "Option::is_none")]
128 pub minimum: Option<f64>,
129
130 #[serde(skip_serializing_if = "Option::is_none")]
132 pub maximum: Option<f64>,
133
134 #[serde(skip_serializing_if = "Option::is_none")]
136 pub exclusive_minimum: Option<f64>,
137
138 #[serde(skip_serializing_if = "Option::is_none")]
140 pub exclusive_maximum: Option<f64>,
141
142 #[serde(skip_serializing_if = "Option::is_none")]
144 pub multiple_of: Option<f64>,
145
146 #[serde(skip_serializing_if = "Option::is_none")]
149 pub items: Option<Box<JsonSchema2020>>,
150
151 #[serde(skip_serializing_if = "Option::is_none")]
153 pub prefix_items: Option<Vec<JsonSchema2020>>,
154
155 #[serde(skip_serializing_if = "Option::is_none")]
157 pub contains: Option<Box<JsonSchema2020>>,
158
159 #[serde(skip_serializing_if = "Option::is_none")]
161 pub min_items: Option<u64>,
162
163 #[serde(skip_serializing_if = "Option::is_none")]
165 pub max_items: Option<u64>,
166
167 #[serde(skip_serializing_if = "Option::is_none")]
169 pub unique_items: Option<bool>,
170
171 #[serde(skip_serializing_if = "Option::is_none")]
174 pub properties: Option<HashMap<String, JsonSchema2020>>,
175
176 #[serde(skip_serializing_if = "Option::is_none")]
178 pub pattern_properties: Option<HashMap<String, JsonSchema2020>>,
179
180 #[serde(skip_serializing_if = "Option::is_none")]
182 pub additional_properties: Option<Box<AdditionalProperties>>,
183
184 #[serde(skip_serializing_if = "Option::is_none")]
186 pub required: Option<Vec<String>>,
187
188 #[serde(skip_serializing_if = "Option::is_none")]
190 pub property_names: Option<Box<JsonSchema2020>>,
191
192 #[serde(skip_serializing_if = "Option::is_none")]
194 pub min_properties: Option<u64>,
195
196 #[serde(skip_serializing_if = "Option::is_none")]
198 pub max_properties: Option<u64>,
199
200 #[serde(skip_serializing_if = "Option::is_none")]
203 pub all_of: Option<Vec<JsonSchema2020>>,
204
205 #[serde(skip_serializing_if = "Option::is_none")]
207 pub any_of: Option<Vec<JsonSchema2020>>,
208
209 #[serde(skip_serializing_if = "Option::is_none")]
211 pub one_of: Option<Vec<JsonSchema2020>>,
212
213 #[serde(skip_serializing_if = "Option::is_none")]
215 pub not: Option<Box<JsonSchema2020>>,
216
217 #[serde(rename = "if", skip_serializing_if = "Option::is_none")]
220 pub if_schema: Option<Box<JsonSchema2020>>,
221
222 #[serde(rename = "then", skip_serializing_if = "Option::is_none")]
224 pub then_schema: Option<Box<JsonSchema2020>>,
225
226 #[serde(rename = "else", skip_serializing_if = "Option::is_none")]
228 pub else_schema: Option<Box<JsonSchema2020>>,
229
230 #[serde(skip_serializing_if = "Option::is_none")]
233 pub deprecated: Option<bool>,
234
235 #[serde(skip_serializing_if = "Option::is_none")]
237 pub read_only: Option<bool>,
238
239 #[serde(skip_serializing_if = "Option::is_none")]
241 pub write_only: Option<bool>,
242
243 #[serde(skip_serializing_if = "Option::is_none")]
245 pub example: Option<serde_json::Value>,
246
247 #[serde(skip_serializing_if = "Option::is_none")]
249 pub examples: Option<Vec<serde_json::Value>>,
250
251 #[serde(skip_serializing_if = "Option::is_none")]
253 pub discriminator: Option<Discriminator>,
254
255 #[serde(skip_serializing_if = "Option::is_none")]
257 pub external_docs: Option<ExternalDocumentation>,
258
259 #[serde(skip_serializing_if = "Option::is_none")]
261 pub xml: Option<Xml>,
262}
263
264#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
266#[serde(untagged)]
267pub enum AdditionalProperties {
268 Bool(bool),
269 Schema(Box<JsonSchema2020>),
270}
271
272#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
274pub struct Discriminator {
275 #[serde(rename = "propertyName")]
277 pub property_name: String,
278
279 #[serde(skip_serializing_if = "Option::is_none")]
281 pub mapping: Option<HashMap<String, String>>,
282}
283
284#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
286pub struct ExternalDocumentation {
287 pub url: String,
289
290 #[serde(skip_serializing_if = "Option::is_none")]
292 pub description: Option<String>,
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
297pub struct Xml {
298 #[serde(skip_serializing_if = "Option::is_none")]
300 pub name: Option<String>,
301
302 #[serde(skip_serializing_if = "Option::is_none")]
304 pub namespace: Option<String>,
305
306 #[serde(skip_serializing_if = "Option::is_none")]
308 pub prefix: Option<String>,
309
310 #[serde(skip_serializing_if = "Option::is_none")]
312 pub attribute: Option<bool>,
313
314 #[serde(skip_serializing_if = "Option::is_none")]
316 pub wrapped: Option<bool>,
317}
318
319impl JsonSchema2020 {
320 pub fn new() -> Self {
322 Self::default()
323 }
324
325 pub fn string() -> Self {
327 Self {
328 schema_type: Some(TypeArray::single("string")),
329 ..Default::default()
330 }
331 }
332
333 pub fn number() -> Self {
335 Self {
336 schema_type: Some(TypeArray::single("number")),
337 ..Default::default()
338 }
339 }
340
341 pub fn integer() -> Self {
343 Self {
344 schema_type: Some(TypeArray::single("integer")),
345 ..Default::default()
346 }
347 }
348
349 pub fn boolean() -> Self {
351 Self {
352 schema_type: Some(TypeArray::single("boolean")),
353 ..Default::default()
354 }
355 }
356
357 pub fn array(items: JsonSchema2020) -> Self {
359 Self {
360 schema_type: Some(TypeArray::single("array")),
361 items: Some(Box::new(items)),
362 ..Default::default()
363 }
364 }
365
366 pub fn object() -> Self {
368 Self {
369 schema_type: Some(TypeArray::single("object")),
370 ..Default::default()
371 }
372 }
373
374 pub fn null() -> Self {
376 Self {
377 schema_type: Some(TypeArray::single("null")),
378 ..Default::default()
379 }
380 }
381
382 pub fn reference(ref_path: impl Into<String>) -> Self {
384 Self {
385 reference: Some(ref_path.into()),
386 ..Default::default()
387 }
388 }
389
390 pub fn nullable(mut self) -> Self {
392 self.schema_type = self.schema_type.map(|t| t.make_nullable());
393 self
394 }
395
396 pub fn with_title(mut self, title: impl Into<String>) -> Self {
398 self.title = Some(title.into());
399 self
400 }
401
402 pub fn with_description(mut self, description: impl Into<String>) -> Self {
404 self.description = Some(description.into());
405 self
406 }
407
408 pub fn with_format(mut self, format: impl Into<String>) -> Self {
410 self.format = Some(format.into());
411 self
412 }
413
414 pub fn with_property(mut self, name: impl Into<String>, schema: JsonSchema2020) -> Self {
416 let properties = self.properties.get_or_insert_with(HashMap::new);
417 properties.insert(name.into(), schema);
418 self
419 }
420
421 pub fn with_required(mut self, name: impl Into<String>) -> Self {
423 let required = self.required.get_or_insert_with(Vec::new);
424 required.push(name.into());
425 self
426 }
427
428 pub fn with_example(mut self, example: serde_json::Value) -> Self {
430 self.example = Some(example);
431 self
432 }
433}
434
435pub struct SchemaTransformer;
437
438impl SchemaTransformer {
439 pub fn transform_30_to_31(schema: serde_json::Value) -> serde_json::Value {
446 match schema {
447 serde_json::Value::Object(mut map) => {
448 if map.get("nullable") == Some(&serde_json::Value::Bool(true)) {
450 map.remove("nullable");
451 if let Some(serde_json::Value::String(ty)) = map.get("type") {
452 let type_array = serde_json::json!([ty.clone(), "null"]);
453 map.insert("type".to_string(), type_array);
454 }
455 }
456
457 if map.get("exclusiveMinimum") == Some(&serde_json::Value::Bool(true)) {
459 if let Some(min) = map.remove("minimum") {
460 map.insert("exclusiveMinimum".to_string(), min);
461 }
462 }
463
464 if map.get("exclusiveMaximum") == Some(&serde_json::Value::Bool(true)) {
466 if let Some(max) = map.remove("maximum") {
467 map.insert("exclusiveMaximum".to_string(), max);
468 }
469 }
470
471 for key in [
473 "items",
474 "additionalProperties",
475 "not",
476 "if",
477 "then",
478 "else",
479 "contains",
480 "propertyNames",
481 ] {
482 if let Some(nested) = map.remove(key) {
483 map.insert(key.to_string(), Self::transform_30_to_31(nested));
484 }
485 }
486
487 for key in ["allOf", "anyOf", "oneOf", "prefixItems"] {
489 if let Some(serde_json::Value::Array(arr)) = map.remove(key) {
490 let transformed: Vec<_> =
491 arr.into_iter().map(Self::transform_30_to_31).collect();
492 map.insert(key.to_string(), serde_json::Value::Array(transformed));
493 }
494 }
495
496 if let Some(serde_json::Value::Object(props)) = map.remove("properties") {
498 let transformed: serde_json::Map<String, serde_json::Value> = props
499 .into_iter()
500 .map(|(k, v)| (k, Self::transform_30_to_31(v)))
501 .collect();
502 map.insert(
503 "properties".to_string(),
504 serde_json::Value::Object(transformed),
505 );
506 }
507
508 if let Some(serde_json::Value::Object(props)) = map.remove("patternProperties") {
510 let transformed: serde_json::Map<String, serde_json::Value> = props
511 .into_iter()
512 .map(|(k, v)| (k, Self::transform_30_to_31(v)))
513 .collect();
514 map.insert(
515 "patternProperties".to_string(),
516 serde_json::Value::Object(transformed),
517 );
518 }
519
520 serde_json::Value::Object(map)
521 }
522 serde_json::Value::Array(arr) => {
523 serde_json::Value::Array(arr.into_iter().map(Self::transform_30_to_31).collect())
524 }
525 other => other,
526 }
527 }
528}
529
530#[cfg(test)]
531mod tests {
532 use super::*;
533
534 #[test]
535 fn test_type_array_single() {
536 let ty = TypeArray::single("string");
537 assert!(!ty.is_nullable());
538 assert_eq!(serde_json::to_string(&ty).unwrap(), r#""string""#);
539 }
540
541 #[test]
542 fn test_type_array_nullable() {
543 let ty = TypeArray::nullable("string");
544 assert!(ty.is_nullable());
545 assert_eq!(serde_json::to_string(&ty).unwrap(), r#"["string","null"]"#);
546 }
547
548 #[test]
549 fn test_make_nullable() {
550 let ty = TypeArray::single("integer").make_nullable();
551 assert!(ty.is_nullable());
552
553 let ty2 = ty.make_nullable();
555 if let TypeArray::Array(types) = ty2 {
556 assert_eq!(types.iter().filter(|t| *t == "null").count(), 1);
557 }
558 }
559
560 #[test]
561 fn test_schema_transformer_nullable() {
562 let schema30 = serde_json::json!({
563 "type": "string",
564 "nullable": true
565 });
566
567 let schema31 = SchemaTransformer::transform_30_to_31(schema30);
568
569 assert_eq!(
570 schema31,
571 serde_json::json!({
572 "type": ["string", "null"]
573 })
574 );
575 }
576
577 #[test]
578 fn test_schema_transformer_exclusive_minimum() {
579 let schema30 = serde_json::json!({
580 "type": "integer",
581 "minimum": 0,
582 "exclusiveMinimum": true
583 });
584
585 let schema31 = SchemaTransformer::transform_30_to_31(schema30);
586
587 assert_eq!(
588 schema31,
589 serde_json::json!({
590 "type": "integer",
591 "exclusiveMinimum": 0
592 })
593 );
594 }
595
596 #[test]
597 fn test_json_schema_2020_builder() {
598 let schema = JsonSchema2020::object()
599 .with_property("name", JsonSchema2020::string())
600 .with_property("age", JsonSchema2020::integer().nullable())
601 .with_required("name");
602
603 assert!(schema.properties.is_some());
604 assert_eq!(schema.required, Some(vec!["name".to_string()]));
605 }
606}