Skip to main content

shape/
jsonschema.rs

1//! JSON Schema export (draft 2020-12): the values a node accepts. Every
2//! kind, bound, format, literal set, composition, reference and default has
3//! a rendering; a check that is a function, and the builders that only
4//! change what comes out (Coerce, Catch, Transform, Rename, Key), have
5//! none. The canonical implementation renders the same schema for the same
6//! shape, and the differential harness compares the two.
7
8use crate::builders::bound_arg;
9use crate::node::{Kind, ListMode, Node, Validator};
10use crate::value::{Map, Value};
11
12pub const JSON_SCHEMA_DRAFT: &str = "https://json-schema.org/draft/2020-12/schema";
13
14/// Render a node as a JSON Schema document.
15pub fn json_schema(n: &Node) -> Value {
16    let mut defs = Map::new();
17    let body = node_schema(n, &mut defs);
18    let mut out = Map::new();
19    out.insert("$schema".to_string(), Value::from(JSON_SCHEMA_DRAFT));
20    for (k, v) in body {
21        out.insert(k, v);
22    }
23    if !defs.is_empty() {
24        out.insert("$defs".to_string(), Value::Obj(defs));
25    }
26    Value::Obj(out)
27}
28
29fn type_name(k: Kind) -> Option<&'static str> {
30    Some(match k {
31        Kind::String | Kind::Date | Kind::Regexp => "string",
32        Kind::Number | Kind::NaN => "number",
33        Kind::Integer => "integer",
34        Kind::Boolean => "boolean",
35        Kind::Null => "null",
36        Kind::Object => "object",
37        Kind::Array => "array",
38        _ => return None,
39    })
40}
41
42fn format_name(builder: &str) -> Option<&'static str> {
43    Some(match builder {
44        "Email" => "email",
45        "Url" => "uri",
46        "Uuid" => "uuid",
47        "DateTime" => "date-time",
48        "Ipv4" => "ipv4",
49        "Ipv6" => "ipv6",
50        _ => return None,
51    })
52}
53
54fn node_schema(n: &Node, defs: &mut Map) -> Map {
55    let mut s = Map::new();
56
57    // A reference stands for the named shape, which is rendered where it
58    // is defined.
59    if let Some(name) = &n.refer_name {
60        s.insert("$ref".to_string(), Value::from(format!("#/$defs/{}", name)));
61        describe_schema(n, &mut s);
62        return s;
63    }
64
65    if let Some(t) = type_name(n.kind) {
66        s.insert("type".to_string(), Value::from(t));
67    }
68
69    match n.kind {
70        Kind::String => {
71            if !n.empty {
72                s.insert("minLength".to_string(), Value::from(1));
73            }
74        }
75        Kind::Date => {
76            s.insert("format".to_string(), Value::from("date-time"));
77        }
78        Kind::Regexp => {
79            s.insert("pattern".to_string(), Value::from(n.regexp_src.as_str()));
80        }
81        Kind::Never => {
82            s.insert("not".to_string(), Value::Obj(Map::new()));
83        }
84        Kind::Object => object_schema(n, &mut s, defs),
85        Kind::Array => array_schema(n, &mut s, defs),
86        Kind::List => list_schema(n, &mut s, defs),
87        _ => {}
88    }
89
90    check_schema(n, &mut s);
91
92    if n.nullable {
93        if let Some(t) = s.get("type").cloned() {
94            s.insert("type".to_string(), Value::Arr(vec![t, Value::from("null")]));
95        }
96    }
97
98    // A default the shape injects. An undefined one is the zero of a kind
99    // that has none (Any, Date); only the null kind's null is a default.
100    let d = &n.default;
101    let carries = !n.required
102        && !n.skippable
103        && n.has_default
104        && !d.is_undefined()
105        && !matches!(d, Value::Func(_))
106        && !matches!(d, Value::Num(f) if f.is_nan())
107        && (!d.is_null() || n.kind == Kind::Null);
108    if carries {
109        s.insert("default".to_string(), d.clone());
110    }
111
112    describe_schema(n, &mut s);
113
114    if let Some(name) = &n.define_name {
115        defs.insert(name.clone(), Value::Obj(s.clone()));
116    }
117    s
118}
119
120fn describe_schema(n: &Node, s: &mut Map) {
121    if let Some(Value::Str(d)) = n.meta.get("description") {
122        s.insert("description".to_string(), Value::Str(d.clone()));
123    }
124}
125
126/// A child shape of Any says nothing, unless it stands for a reference.
127fn is_any_schema(child: &Node) -> bool {
128    child.kind == Kind::Any && child.refer_name.is_none()
129}
130
131fn object_schema(n: &Node, s: &mut Map, defs: &mut Map) {
132    let mut props = Map::new();
133    let mut required: Vec<String> = Vec::new();
134    for (k, cn) in &n.obj_children {
135        props.insert(k.clone(), Value::Obj(node_schema(cn, defs)));
136        if cn.required {
137            required.push(k.clone());
138        }
139    }
140    if !props.is_empty() {
141        s.insert("properties".to_string(), Value::Obj(props));
142    }
143    if !required.is_empty() {
144        required.sort();
145        s.insert(
146            "required".to_string(),
147            Value::Arr(required.into_iter().map(Value::Str).collect()),
148        );
149    }
150    match &n.obj_rest {
151        None => {
152            s.insert("additionalProperties".to_string(), Value::Bool(false));
153        }
154        Some(rest) if !is_any_schema(rest) => {
155            s.insert(
156                "additionalProperties".to_string(),
157                Value::Obj(node_schema(rest, defs)),
158            );
159        }
160        Some(_) => {}
161    }
162}
163
164fn array_schema(n: &Node, s: &mut Map, defs: &mut Map) {
165    // A tail past the tuple, or every element: one child slot serves both,
166    // so Rest replaces the repeating shape. An element shape of Any says
167    // nothing; nothing may follow a closed tuple.
168    let child = n.arr_rest.as_deref().or(n.arr_child.as_deref());
169    let closed = child.is_none();
170    let child = child.filter(|c| !is_any_schema(c));
171    if !n.arr_children.is_empty() {
172        let fixed: Vec<Value> = n
173            .arr_children
174            .iter()
175            .map(|cn| Value::Obj(node_schema(cn, defs)))
176            .collect();
177        s.insert("prefixItems".to_string(), Value::Arr(fixed));
178        if closed {
179            s.insert("items".to_string(), Value::Bool(false));
180        } else if let Some(c) = child {
181            s.insert("items".to_string(), Value::Obj(node_schema(c, defs)));
182        }
183    } else if let Some(c) = child {
184        s.insert("items".to_string(), Value::Obj(node_schema(c, defs)));
185    }
186}
187
188fn list_schema(n: &Node, s: &mut Map, defs: &mut Map) {
189    let mut branches: Vec<Value> = n
190        .list
191        .iter()
192        .map(|bn| Value::Obj(node_schema(bn, defs)))
193        .collect();
194    if let Some(d) = &n.disc {
195        for (i, b) in branches.iter_mut().enumerate() {
196            let Value::Obj(bs) = b else { continue };
197            // An object branch carries the tag as a key; any other branch
198            // gets the tag as its only property.
199            let mut props = match bs.shift_remove("properties") {
200                Some(Value::Obj(p)) => p,
201                _ => Map::new(),
202            };
203            let mut tag_schema = Map::new();
204            tag_schema.insert("type".to_string(), Value::from("string"));
205            tag_schema.insert("const".to_string(), Value::from(d.tags[i].as_str()));
206            props.insert(d.tag.clone(), Value::Obj(tag_schema));
207            bs.insert("properties".to_string(), Value::Obj(props));
208            let mut required = vec![d.tag.clone()];
209            if let Some(Value::Arr(have)) = bs.get("required") {
210                for k in have.iter().filter_map(|k| k.as_str()) {
211                    if k != d.tag {
212                        required.push(k.to_string());
213                    }
214                }
215            }
216            required.sort();
217            bs.insert(
218                "required".to_string(),
219                Value::Arr(required.into_iter().map(Value::Str).collect()),
220            );
221        }
222        s.insert("oneOf".to_string(), Value::Arr(branches));
223        return;
224    }
225    let key = if n.list_mode == ListMode::All {
226        "allOf"
227    } else {
228        "anyOf"
229    };
230    s.insert(key.to_string(), Value::Arr(branches));
231}
232
233/// The bounds a size builder puts on a value: the number family for a
234/// number, a length family for a string, array or object, and every family
235/// for a node that has not said.
236fn size_families(k: Kind) -> &'static [&'static str] {
237    match k {
238        Kind::Number | Kind::NaN | Kind::Integer => &["minimum"],
239        Kind::String => &["minLength"],
240        Kind::Array => &["minItems"],
241        Kind::Object => &["minProperties"],
242        _ => &["minimum", "minLength", "minItems", "minProperties"],
243    }
244}
245
246fn size_max(lo: &str) -> &'static str {
247    match lo {
248        "minimum" => "maximum",
249        "minLength" => "maxLength",
250        "minItems" => "maxItems",
251        _ => "maxProperties",
252    }
253}
254
255fn check_schema(n: &Node, s: &mut Map) {
256    let families = size_families(n.kind);
257    let mut vs: Vec<&Validator> = n.befores.iter().chain(n.afters.iter()).collect();
258    let mut i = 0;
259    while i < vs.len() {
260        let v = vs[i];
261        i += 1;
262        // Catch and Transform take the node's checks inside.
263        if let Some(inner) = &v.inner {
264            vs.extend(inner.befores.iter().chain(inner.afters.iter()));
265            continue;
266        }
267        match v.name.as_str() {
268            "Exact" => {
269                s.insert("enum".to_string(), Value::Arr(v.args.clone()));
270            }
271            "Ip" => {
272                let fmt = |f: &str| {
273                    let mut m = Map::new();
274                    m.insert("format".to_string(), Value::from(f));
275                    Value::Obj(m)
276                };
277                s.insert(
278                    "anyOf".to_string(),
279                    Value::Arr(vec![fmt("ipv4"), fmt("ipv6")]),
280                );
281            }
282            "Min" | "Max" | "Above" | "Below" | "Len" => {
283                let size = v.args.first().and_then(bound_arg).unwrap_or(0.0);
284                for lo in families {
285                    let hi = size_max(lo);
286                    let numeric = *lo == "minimum";
287                    match v.name.as_str() {
288                        "Min" => {
289                            s.insert(lo.to_string(), Value::Num(size));
290                        }
291                        "Max" => {
292                            s.insert(hi.to_string(), Value::Num(size));
293                        }
294                        "Above" => {
295                            if numeric {
296                                s.insert("exclusiveMinimum".to_string(), Value::Num(size));
297                            } else {
298                                s.insert(lo.to_string(), Value::Num(size + 1.0));
299                            }
300                        }
301                        "Below" => {
302                            if numeric {
303                                s.insert("exclusiveMaximum".to_string(), Value::Num(size));
304                            } else {
305                                s.insert(hi.to_string(), Value::Num(size - 1.0));
306                            }
307                        }
308                        _ => {
309                            s.insert(lo.to_string(), Value::Num(size));
310                            s.insert(hi.to_string(), Value::Num(size));
311                        }
312                    }
313                }
314            }
315            name => {
316                if let Some(f) = format_name(name) {
317                    s.insert("format".to_string(), Value::from(f));
318                } else if name.len() >= 2 && name.starts_with('/') && name.ends_with('/') {
319                    s.insert("pattern".to_string(), Value::from(&name[1..name.len() - 1]));
320                }
321            }
322        }
323    }
324}
325
326#[cfg(test)]
327mod tests {
328    use super::*;
329    use crate::builders::*;
330    use crate::discriminated::discriminated;
331    use crate::node::Token;
332    use crate::spec::{arr, obj, Spec};
333    use crate::Schema;
334    use regex::Regex;
335    use serde_json::json;
336
337    fn js(spec: impl Into<Spec>) -> serde_json::Value {
338        let mut v = serde_json::Value::from(Schema::new(spec).json_schema());
339        v.as_object_mut().unwrap().remove("$schema");
340        v
341    }
342
343    #[test]
344    fn renders_kinds_and_containers() {
345        assert_eq!(
346            serde_json::Value::from(Schema::new(Token::String).json_schema()),
347            json!({"$schema": JSON_SCHEMA_DRAFT, "type": "string", "minLength": 1})
348        );
349        assert_eq!(js(empty(Token::String)), json!({"type": "string"}));
350        assert_eq!(
351            js("x"),
352            json!({"type": "string", "minLength": 1, "default": "x"})
353        );
354        assert_eq!(
355            js(Token::Date),
356            json!({"type": "string", "format": "date-time"})
357        );
358        assert_eq!(
359            js(Regex::new("^a").unwrap()),
360            json!({"type": "string", "pattern": "^a"})
361        );
362        assert_eq!(js(never(any())), json!({"not": {}}));
363        assert_eq!(js(Token::Any), json!({}));
364        assert_eq!(js(Token::Integer), json!({"type": "integer"}));
365        assert_eq!(js(f64::NAN), json!({"type": "number"}));
366        assert_eq!(
367            js(Spec::Value(Value::Null)),
368            json!({"type": "null", "default": null})
369        );
370        assert_eq!(js(Spec::Value(Value::Func(1))), json!({}));
371        assert_eq!(js(Token::Function), json!({}));
372        assert_eq!(
373            js(nullable(Token::Number)),
374            json!({"type": ["number", "null"]})
375        );
376        assert_eq!(js(nullable(any())), json!({}));
377        assert_eq!(
378            js(describe("d", Token::Boolean)),
379            json!({"type": "boolean", "description": "d"})
380        );
381        assert_eq!(
382            js(obj([
383                ("a", Spec::from(Token::Number)),
384                ("b", Spec::from(1))
385            ])),
386            json!({"type": "object", "properties": {"a": {"type": "number"}, "b": {"type": "number", "default": 1}}, "required": ["a"], "additionalProperties": false})
387        );
388        assert_eq!(js(Token::Object), json!({"type": "object"}));
389        assert_eq!(
390            js(optional(Token::Object)),
391            json!({"type": "object", "default": {}})
392        );
393        assert_eq!(
394            js(child(Token::Number, Token::Object)),
395            json!({"type": "object", "additionalProperties": {"type": "number"}})
396        );
397        assert_eq!(js(Token::Array), json!({"type": "array"}));
398        assert_eq!(
399            js(arr([Spec::from(Token::Number)])),
400            json!({"type": "array", "items": {"type": "number"}})
401        );
402        assert_eq!(
403            js(arr([Spec::from(Token::Number), Spec::from("x")])),
404            json!({"type": "array", "prefixItems": [{"type": "number"}, {"type": "string", "minLength": 1, "default": "x"}], "items": false})
405        );
406        assert_eq!(
407            js(rest(
408                Token::String,
409                arr([Spec::from(Token::Number), Spec::from(1)])
410            )),
411            json!({"type": "array", "prefixItems": [{"type": "number"}, {"type": "number", "default": 1}], "items": {"type": "string", "minLength": 1}})
412        );
413        assert_eq!(
414            js(rest(
415                Token::Any,
416                arr([Spec::from(Token::Number), Spec::from(1)])
417            )),
418            json!({"type": "array", "prefixItems": [{"type": "number"}, {"type": "number", "default": 1}]})
419        );
420        assert_eq!(js(arr(Vec::<Spec>::new())), json!({"type": "array"}));
421    }
422
423    #[test]
424    fn renders_checks_and_compositions() {
425        assert_eq!(js(exact(["a", "b"])), json!({"enum": ["a", "b"]}));
426        assert_eq!(
427            js(email(any())),
428            json!({"type": "string", "minLength": 1, "format": "email"})
429        );
430        assert_eq!(
431            js(url(any())),
432            json!({"type": "string", "minLength": 1, "format": "uri"})
433        );
434        assert_eq!(
435            js(uuid(any())),
436            json!({"type": "string", "minLength": 1, "format": "uuid"})
437        );
438        assert_eq!(
439            js(date_time(any())),
440            json!({"type": "string", "minLength": 1, "format": "date-time"})
441        );
442        assert_eq!(
443            js(ipv4(any())),
444            json!({"type": "string", "minLength": 1, "format": "ipv4"})
445        );
446        assert_eq!(
447            js(ipv6(any())),
448            json!({"type": "string", "minLength": 1, "format": "ipv6"})
449        );
450        assert_eq!(
451            js(ip(any())),
452            json!({"type": "string", "minLength": 1, "anyOf": [{"format": "ipv4"}, {"format": "ipv6"}]})
453        );
454        assert_eq!(
455            js(min(2, Token::Number)),
456            json!({"type": "number", "minimum": 2})
457        );
458        assert_eq!(
459            js(max(2, Token::Integer)),
460            json!({"type": "integer", "maximum": 2})
461        );
462        assert_eq!(
463            js(above(2, Token::Number)),
464            json!({"type": "number", "exclusiveMinimum": 2})
465        );
466        assert_eq!(
467            js(below(2, Token::Number)),
468            json!({"type": "number", "exclusiveMaximum": 2})
469        );
470        assert_eq!(
471            js(len(2, Token::Number)),
472            json!({"type": "number", "minimum": 2, "maximum": 2})
473        );
474        assert_eq!(
475            js(above(2, Token::String)),
476            json!({"type": "string", "minLength": 3})
477        );
478        assert_eq!(
479            js(below(2, Token::String)),
480            json!({"type": "string", "minLength": 1, "maxLength": 1})
481        );
482        assert_eq!(
483            js(min(2, Token::Array)),
484            json!({"type": "array", "minItems": 2})
485        );
486        assert_eq!(
487            js(max(2, Token::Object)),
488            json!({"type": "object", "maxProperties": 2})
489        );
490        assert_eq!(
491            js(min(2, any())),
492            json!({"minimum": 2, "minLength": 2, "minItems": 2, "minProperties": 2})
493        );
494        assert_eq!(
495            js(len(2, any())),
496            json!({"minimum": 2, "maximum": 2, "minLength": 2, "maxLength": 2, "minItems": 2, "maxItems": 2, "minProperties": 2, "maxProperties": 2})
497        );
498        assert_eq!(
499            js(above(2, any())),
500            json!({"exclusiveMinimum": 2, "minLength": 3, "minItems": 3, "minProperties": 3})
501        );
502        assert_eq!(
503            js(below(2, any())),
504            json!({"exclusiveMaximum": 2, "maxLength": 1, "maxItems": 1, "maxProperties": 1})
505        );
506        assert_eq!(
507            js(check_re(Regex::new("^a").unwrap(), Token::String)),
508            json!({"type": "string", "minLength": 1, "pattern": "^a"})
509        );
510        assert_eq!(
511            js(check(|_, _| true, Token::String)),
512            json!({"type": "string", "minLength": 1})
513        );
514        assert_eq!(
515            js(catch(0, min(2, Token::Number))),
516            json!({"type": "number", "minimum": 2})
517        );
518        assert_eq!(
519            js(coerce(transform(|v, _| v, max(2, Token::Number)))),
520            json!({"type": "number", "maximum": 2})
521        );
522        assert_eq!(
523            js(one([Spec::from(Token::String), Spec::from(Token::Number)])),
524            json!({"anyOf": [{"type": "string", "minLength": 1}, {"type": "number"}]})
525        );
526        assert_eq!(
527            js(all([Spec::from(Token::Number), Spec::from(min(1, any()))])),
528            json!({"allOf": [{"type": "number"}, {"minimum": 1, "minLength": 1, "minItems": 1, "minProperties": 1}]})
529        );
530        assert_eq!(
531            js(discriminated(
532                "k",
533                [
534                    ("a", obj([("x", Spec::from(Token::Number))])),
535                    ("b", Spec::from(Token::Number))
536                ]
537            )),
538            json!({"oneOf": [
539                {"type": "object", "properties": {"x": {"type": "number"}, "k": {"type": "string", "const": "a"}}, "required": ["k", "x"], "additionalProperties": false},
540                {"type": "number", "properties": {"k": {"type": "string", "const": "b"}}, "required": ["k"]}
541            ]})
542        );
543        assert_eq!(
544            js(obj([
545                ("a", Spec::from(define("d", Token::Number))),
546                ("b", Spec::from(refer("d", any())))
547            ])),
548            json!({"type": "object", "properties": {"a": {"type": "number"}, "b": {"$ref": "#/$defs/d"}}, "required": ["a"], "additionalProperties": false, "$defs": {"d": {"type": "number"}}})
549        );
550        assert_eq!(
551            js(describe("r", refer("d", any()))),
552            json!({"$ref": "#/$defs/d", "description": "r"})
553        );
554        assert_eq!(
555            js(child(refer("d", any()), Token::Object)),
556            json!({"type": "object", "additionalProperties": {"$ref": "#/$defs/d"}})
557        );
558        assert_eq!(
559            js(arr([Spec::from(refer("d", any()))])),
560            json!({"type": "array", "items": {"$ref": "#/$defs/d"}})
561        );
562        assert_eq!(js(skip(Token::Number)), json!({"type": "number"}));
563        // A branch that declares the tag itself lists it once.
564        assert_eq!(
565            js(discriminated(
566                "k",
567                [("a", obj([("k", Spec::from(Token::String))]))]
568            )),
569            json!({"oneOf": [{"type": "object", "properties": {"k": {"type": "string", "const": "a"}}, "required": ["k"], "additionalProperties": false}]})
570        );
571        assert_eq!(
572            js(optional(Token::Date)),
573            json!({"type": "string", "format": "date-time"})
574        );
575        assert_eq!(
576            serde_json::Value::from(json_schema(&buildize(Token::Number))),
577            json!({"$schema": JSON_SCHEMA_DRAFT, "type": "number"})
578        );
579    }
580}