1extern crate alloc;
24
25use alloc::collections::BTreeMap;
26use alloc::string::String;
27use alloc::vec::Vec;
28
29use facet::Facet;
30use facet_core::{Def, Field, Shape, StructKind, Type, UserType};
31
32#[derive(Debug, Clone, Facet)]
37#[facet(skip_all_unless_truthy)]
38pub struct JsonSchema {
39 #[facet(rename = "$schema")]
41 pub schema: Option<String>,
42
43 #[facet(rename = "$ref")]
45 pub ref_: Option<String>,
46
47 #[facet(rename = "$defs")]
49 pub defs: Option<BTreeMap<String, JsonSchema>>,
50
51 #[facet(rename = "type")]
53 pub type_: Option<SchemaTypes>,
54
55 pub properties: Option<BTreeMap<String, JsonSchema>>,
57
58 pub required: Option<Vec<String>>,
60
61 #[facet(rename = "additionalProperties")]
63 pub additional_properties: Option<AdditionalProperties>,
64
65 pub items: Option<Box<JsonSchema>>,
67
68 #[facet(rename = "enum")]
70 pub enum_: Option<Vec<String>>,
71
72 pub minimum: Option<i64>,
74
75 pub maximum: Option<i64>,
77
78 #[facet(rename = "oneOf")]
80 pub one_of: Option<Vec<JsonSchema>>,
81
82 #[facet(rename = "anyOf")]
83 pub any_of: Option<Vec<JsonSchema>>,
84
85 #[facet(rename = "allOf")]
86 pub all_of: Option<Vec<JsonSchema>>,
87
88 pub description: Option<String>,
90
91 pub title: Option<String>,
93
94 #[facet(rename = "const")]
96 pub const_: Option<String>,
97}
98
99#[derive(Debug, Clone, Facet)]
101#[facet(rename_all = "lowercase")]
102#[repr(u8)]
103pub enum SchemaType {
104 String,
105 Number,
106 Integer,
107 Boolean,
108 Array,
109 Object,
110 Null,
111}
112
113#[derive(Debug, Clone, Facet)]
115#[facet(untagged)]
116#[repr(u8)]
117pub enum SchemaTypes {
118 Single(SchemaType),
119 Multiple(Vec<SchemaType>),
120}
121
122impl From<SchemaType> for SchemaTypes {
123 fn from(value: SchemaType) -> Self {
124 Self::Single(value)
125 }
126}
127
128#[derive(Debug, Clone, Facet)]
130#[facet(untagged)]
131#[repr(u8)]
132pub enum AdditionalProperties {
133 Bool(bool),
134 Schema(Box<JsonSchema>),
135}
136
137impl Default for JsonSchema {
138 fn default() -> Self {
139 Self::new()
140 }
141}
142
143impl JsonSchema {
144 pub const fn new() -> Self {
146 Self {
147 schema: None,
148 ref_: None,
149 defs: None,
150 type_: None,
151 properties: None,
152 required: None,
153 additional_properties: None,
154 items: None,
155 enum_: None,
156 minimum: None,
157 maximum: None,
158 one_of: None,
159 any_of: None,
160 all_of: None,
161 description: None,
162 title: None,
163 const_: None,
164 }
165 }
166
167 pub fn with_dialect(dialect: &str) -> Self {
169 Self {
170 schema: Some(dialect.into()),
171 ..Self::new()
172 }
173 }
174
175 pub fn reference(ref_path: &str) -> Self {
177 Self {
178 ref_: Some(ref_path.into()),
179 ..Self::new()
180 }
181 }
182}
183
184pub fn schema_for<T: Facet<'static>>() -> JsonSchema {
188 let mut ctx = SchemaContext::new();
189 let schema = ctx.schema_for_shape(T::SHAPE);
190
191 if ctx.defs.is_empty() {
193 schema
194 } else {
195 JsonSchema {
196 schema: Some("https://json-schema.org/draft/2020-12/schema".into()),
197 defs: Some(ctx.defs),
198 ..schema
199 }
200 }
201}
202
203pub fn to_schema<T: Facet<'static>>() -> String {
205 let schema = schema_for::<T>();
206 facet_json::to_string_pretty(&schema).expect("JSON Schema serialization should not fail")
207}
208
209struct SchemaContext {
211 defs: BTreeMap<String, JsonSchema>,
213 in_progress: Vec<&'static str>,
215}
216
217impl SchemaContext {
218 const fn new() -> Self {
219 Self {
220 defs: BTreeMap::new(),
221 in_progress: Vec::new(),
222 }
223 }
224
225 fn schema_for_shape(&mut self, shape: &'static Shape) -> JsonSchema {
226 let type_name = shape.type_identifier;
228 if self.in_progress.contains(&type_name) {
229 return JsonSchema::reference(&format!("#/$defs/{}", type_name));
230 }
231
232 let description = if shape.doc.is_empty() {
234 None
235 } else {
236 Some(shape.doc.join("\n").trim().to_string())
237 };
238
239 match &shape.def {
244 Def::Scalar => self.schema_for_scalar(shape, description),
245 Def::Option(opt) => {
246 let inner_schema = self.schema_for_shape(opt.t);
248 JsonSchema {
249 any_of: Some(vec![
250 inner_schema,
251 JsonSchema {
252 type_: Some(SchemaType::Null.into()),
253 ..JsonSchema::new()
254 },
255 ]),
256 description,
257 ..JsonSchema::new()
258 }
259 }
260 Def::List(list) => JsonSchema {
261 type_: Some(SchemaType::Array.into()),
262 items: Some(Box::new(self.schema_for_shape(list.t))),
263 description,
264 ..JsonSchema::new()
265 },
266 Def::Array(arr) => JsonSchema {
267 type_: Some(SchemaType::Array.into()),
268 items: Some(Box::new(self.schema_for_shape(arr.t))),
269 description,
270 ..JsonSchema::new()
271 },
272 Def::Set(set) => JsonSchema {
273 type_: Some(SchemaType::Array.into()),
274 items: Some(Box::new(self.schema_for_shape(set.t))),
275 description,
276 ..JsonSchema::new()
277 },
278 Def::Map(map) => {
279 JsonSchema {
281 type_: Some(SchemaType::Object.into()),
282 additional_properties: Some(AdditionalProperties::Schema(Box::new(
283 self.schema_for_shape(map.v),
284 ))),
285 description,
286 ..JsonSchema::new()
287 }
288 }
289 Def::Undefined => {
290 match &shape.ty {
292 Type::User(UserType::Struct(st)) => {
293 self.schema_for_struct(shape, st.fields, st.kind, description)
294 }
295 Type::User(UserType::Enum(en)) => self.schema_for_enum(shape, en, description),
296 _ => {
297 if let Some(inner) = shape.inner {
299 self.schema_for_shape(inner)
300 } else {
301 JsonSchema {
302 description,
303 ..JsonSchema::new()
304 }
305 }
306 }
307 }
308 }
309 _ => {
310 if let Some(inner) = shape.inner {
312 self.schema_for_shape(inner)
313 } else {
314 JsonSchema {
315 description,
316 ..JsonSchema::new()
317 }
318 }
319 }
320 }
321 }
322
323 fn schema_for_scalar(
324 &mut self,
325 shape: &'static Shape,
326 description: Option<String>,
327 ) -> JsonSchema {
328 let type_name = shape.type_identifier;
329
330 let (type_, minimum, maximum) = match type_name {
332 "String" | "str" | "&str" | "Cow" => (Some(SchemaType::String.into()), None, None),
334
335 "bool" => (Some(SchemaType::Boolean.into()), None, None),
337
338 "u8" | "u16" | "u32" | "u64" | "u128" | "usize" => {
340 (Some(SchemaType::Integer.into()), Some(0), None)
341 }
342
343 "i8" => (Some(SchemaType::Integer.into()), Some(i8::MIN as i64), None),
345 "i16" => (
346 Some(SchemaType::Integer.into()),
347 Some(i16::MIN as i64),
348 None,
349 ),
350 "i32" => (
351 Some(SchemaType::Integer.into()),
352 Some(i32::MIN as i64),
353 None,
354 ),
355 "i64" => (Some(SchemaType::Integer.into()), Some(i64::MIN), None),
356 "i128" => (Some(SchemaType::Integer.into()), Some(i64::MIN), None),
357 "isize" => (Some(SchemaType::Integer.into()), Some(i64::MIN), None),
358
359 "f32" | "f64" => (Some(SchemaType::Number.into()), None, None),
361
362 "char" => (Some(SchemaType::String.into()), None, None),
364
365 _ => (None, None, None),
367 };
368
369 JsonSchema {
370 type_,
371 minimum,
372 maximum,
373 description,
374 ..JsonSchema::new()
375 }
376 }
377
378 fn schema_for_struct(
379 &mut self,
380 shape: &'static Shape,
381 fields: &'static [Field],
382 kind: StructKind,
383 description: Option<String>,
384 ) -> JsonSchema {
385 match kind {
386 StructKind::Unit => {
387 JsonSchema {
389 type_: Some(SchemaType::Null.into()),
390 description,
391 ..JsonSchema::new()
392 }
393 }
394 StructKind::TupleStruct if fields.len() == 1 => {
395 self.schema_for_shape(fields[0].shape.get())
397 }
398 StructKind::TupleStruct | StructKind::Tuple => {
399 let _items: Vec<JsonSchema> = fields
401 .iter()
402 .map(|f| self.schema_for_shape(f.shape.get()))
403 .collect();
404
405 JsonSchema {
407 type_: Some(SchemaType::Array.into()),
408 description,
409 ..JsonSchema::new()
410 }
411 }
412 StructKind::Struct => {
413 self.in_progress.push(shape.type_identifier);
415
416 let mut properties = BTreeMap::new();
417 let mut required = Vec::new();
418
419 for field in fields {
420 if field.flags.contains(facet_core::FieldFlags::SKIP) {
422 continue;
423 }
424
425 let field_name = field.effective_name();
426 let field_schema = self.schema_for_shape(field.shape.get());
427
428 let is_option = matches!(field.shape.get().def, Def::Option(_));
430 let has_default = field.default.is_some();
431
432 if !is_option && !has_default {
433 required.push(field_name.to_string());
434 }
435
436 properties.insert(field_name.to_string(), field_schema);
437 }
438
439 self.in_progress.pop();
440
441 JsonSchema {
442 type_: Some(SchemaType::Object.into()),
443 properties: Some(properties),
444 required: if required.is_empty() {
445 None
446 } else {
447 Some(required)
448 },
449 additional_properties: Some(AdditionalProperties::Bool(false)),
450 description,
451 title: Some(shape.type_identifier.to_string()),
452 ..JsonSchema::new()
453 }
454 }
455 }
456 }
457
458 fn schema_for_enum(
459 &mut self,
460 shape: &'static Shape,
461 enum_type: &facet_core::EnumType,
462 description: Option<String>,
463 ) -> JsonSchema {
464 let all_unit = enum_type
466 .variants
467 .iter()
468 .all(|v| matches!(v.data.kind, StructKind::Unit));
469
470 if all_unit {
471 let values: Vec<String> = enum_type
473 .variants
474 .iter()
475 .map(|v| v.effective_name().to_string())
476 .collect();
477
478 JsonSchema {
479 type_: Some(SchemaType::String.into()),
480 enum_: Some(values),
481 description,
482 title: Some(shape.type_identifier.to_string()),
483 ..JsonSchema::new()
484 }
485 } else {
486 let variants: Vec<JsonSchema> = enum_type
489 .variants
490 .iter()
491 .map(|v| {
492 let variant_name = v.effective_name().to_string();
493 match v.data.kind {
494 StructKind::Unit => {
495 JsonSchema {
497 const_: Some(variant_name),
498 ..JsonSchema::new()
499 }
500 }
501 StructKind::TupleStruct if v.data.fields.len() == 1 => {
502 let mut props = BTreeMap::new();
504 props.insert(
505 variant_name.clone(),
506 self.schema_for_shape(v.data.fields[0].shape.get()),
507 );
508 JsonSchema {
509 type_: Some(SchemaType::Object.into()),
510 properties: Some(props),
511 required: Some(vec![variant_name]),
512 additional_properties: Some(AdditionalProperties::Bool(false)),
513 ..JsonSchema::new()
514 }
515 }
516 _ => {
517 let inner =
519 self.schema_for_struct(shape, v.data.fields, v.data.kind, None);
520 let mut props = BTreeMap::new();
521 props.insert(variant_name.clone(), inner);
522 JsonSchema {
523 type_: Some(SchemaType::Object.into()),
524 properties: Some(props),
525 required: Some(vec![variant_name]),
526 additional_properties: Some(AdditionalProperties::Bool(false)),
527 ..JsonSchema::new()
528 }
529 }
530 }
531 })
532 .collect();
533
534 JsonSchema {
535 one_of: Some(variants),
536 description,
537 title: Some(shape.type_identifier.to_string()),
538 ..JsonSchema::new()
539 }
540 }
541 }
542}
543
544#[cfg(test)]
545mod tests {
546 use super::*;
547
548 #[test]
549 fn test_simple_struct() {
550 #[derive(Facet)]
551 struct User {
552 name: String,
553 age: u32,
554 }
555
556 let schema = to_schema::<User>();
557 insta::assert_snapshot!(schema);
558 }
559
560 #[test]
561 fn test_optional_field() {
562 #[derive(Facet)]
563 struct Config {
564 required: String,
565 optional: Option<String>,
566 }
567
568 let schema = to_schema::<Config>();
569 insta::assert_snapshot!(schema);
570 }
571
572 #[test]
573 fn test_simple_enum() {
574 #[derive(Facet)]
575 #[repr(u8)]
576 enum Status {
577 Active,
578 Inactive,
579 Pending,
580 }
581
582 let schema = to_schema::<Status>();
583 insta::assert_snapshot!(schema);
584 }
585
586 #[test]
587 fn test_vec() {
588 #[derive(Facet)]
589 struct Data {
590 items: Vec<String>,
591 }
592
593 let schema = to_schema::<Data>();
594 insta::assert_snapshot!(schema);
595 }
596
597 #[test]
598 fn test_enum_rename_all_snake_case() {
599 #[derive(Facet)]
600 #[facet(rename_all = "snake_case")]
601 #[repr(u8)]
602 enum ValidationErrorCode {
603 CircularDependency,
604 InvalidNaming,
605 UnknownRequirement,
606 }
607
608 let schema = to_schema::<ValidationErrorCode>();
609 insta::assert_snapshot!(schema);
610 }
611
612 #[test]
613 fn test_struct_rename_all_camel_case() {
614 #[derive(Facet)]
615 #[facet(rename_all = "camelCase")]
616 struct ApiResponse {
617 user_name: String,
618 created_at: String,
619 is_active: bool,
620 }
621
622 let schema = to_schema::<ApiResponse>();
623 insta::assert_snapshot!(schema);
624 }
625
626 #[test]
627 fn test_enum_with_data_rename_all() {
628 #[allow(dead_code)]
629 #[derive(Facet)]
630 #[facet(rename_all = "snake_case")]
631 #[repr(C)]
632 enum Message {
633 TextMessage { content: String },
634 ImageUpload { url: String, width: u32 },
635 }
636
637 let schema = to_schema::<Message>();
638 insta::assert_snapshot!(schema);
639 }
640
641 #[test]
642 fn test_deserialize_schema_type_as_string() {
643 let schema: JsonSchema =
644 facet_json::from_str_borrowed(r#"{"type":"integer"}"#).expect("valid schema JSON");
645
646 match schema.type_ {
647 Some(SchemaTypes::Single(SchemaType::Integer)) => {}
648 other => panic!("expected single integer type, got {other:?}"),
649 }
650 }
651
652 #[test]
653 fn test_deserialize_schema_type_as_array() {
654 let schema: JsonSchema =
655 facet_json::from_str_borrowed(r#"{"type":["integer"]}"#).expect("valid schema JSON");
656
657 match schema.type_ {
658 Some(SchemaTypes::Multiple(types)) => {
659 assert!(matches!(types.as_slice(), [SchemaType::Integer]));
660 }
661 other => panic!("expected integer type array, got {other:?}"),
662 }
663 }
664}