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