json_schema_generator/
lib.rs

1use serde_json::{json, Map, Value};
2
3pub fn generate_json_schema(instance: &Value) -> Value {
4    match instance {
5        Value::Object(_) => generate_object_schema(instance),
6        Value::Array(arr) => generate_array_schema(arr),
7        Value::String(_) => json!({"type": "string"}),
8        Value::Number(n) => {
9            if n.is_i64() {
10                json!({"type": "integer"})
11            } else {
12                json!({"type": "number"})
13            }
14        }
15        Value::Bool(_) => json!({"type": "boolean"}),
16        Value::Null => json!({"type": "null"}),
17    }
18}
19
20fn generate_object_schema(instance: &Value) -> Value {
21    let mut schema = json!({
22        "type": "object",
23        "properties": {},
24        "required": []
25    });
26
27    if let Value::Object(obj) = instance {
28        for (key, value) in obj {
29            if key == "$ref" {
30                schema["$ref"] = value.clone();
31            } else {
32                let mut sub_schema = generate_json_schema(value);
33                if let Some(obj) = sub_schema.as_object_mut() {
34                    obj.remove("$schema"); // Remove $schema from nested objects
35                }
36                schema["properties"][key] = sub_schema;
37                schema["required"]
38                    .as_array_mut()
39                    .unwrap()
40                    .push(Value::String(key.clone()));
41            }
42        }
43    }
44
45    // Sort the "required" array
46    if let Some(required) = schema["required"].as_array_mut() {
47        required.sort_by(|a, b| a.as_str().unwrap().cmp(b.as_str().unwrap()));
48    }
49
50    // Add $schema only to the top-level object
51    schema["$schema"] = json!("http://json-schema.org/draft-07/schema#");
52
53    schema
54}
55
56fn generate_array_schema(arr: &Vec<Value>) -> Value {
57    if arr.is_empty() {
58        return json!({
59            "type": "array",
60            "items": {}
61        });
62    }
63
64    let item_schemas: Vec<Value> = arr.iter().map(generate_json_schema).collect();
65    let common_schema = find_common_schema(&item_schemas);
66
67    json!({
68        "type": "array",
69        "items": common_schema
70    })
71}
72
73fn find_common_schema(schemas: &[Value]) -> Value {
74    if schemas.is_empty() {
75        return json!({});
76    }
77
78    let mut common = schemas[0].clone();
79    for schema in schemas.iter().skip(1) {
80        common = merge_schemas(&common, schema);
81    }
82
83    common
84}
85
86fn merge_schemas(schema1: &Value, schema2: &Value) -> Value {
87    if schema1 == schema2 {
88        return schema1.clone();
89    }
90
91    let mut merged = json!({
92        "oneOf": [schema1, schema2]
93    });
94
95    if let (Value::Object(obj1), Value::Object(obj2)) = (schema1, schema2) {
96        if obj1.get("type") == obj2.get("type") {
97            merged = json!({
98                "type": obj1["type"].clone()
99            });
100
101            if obj1.contains_key("properties") && obj2.contains_key("properties") {
102                let mut properties = Map::new();
103                let props1 = obj1["properties"].as_object().unwrap();
104                let props2 = obj2["properties"].as_object().unwrap();
105
106                for (key, value) in props1.iter().chain(props2.iter()) {
107                    properties.insert(key.clone(), value.clone());
108                }
109
110                merged["properties"] = Value::Object(properties);
111            }
112        }
113    }
114
115    merged
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use serde_json::json;
122
123    #[test]
124    fn test_generate_json_schema_string() {
125        let input = json!("test");
126        let expected = json!({"type": "string"});
127        assert_eq!(generate_json_schema(&input), expected);
128    }
129
130    #[test]
131    fn test_generate_json_schema_integer() {
132        let input = json!(42);
133        let expected = json!({"type": "integer"});
134        assert_eq!(generate_json_schema(&input), expected);
135    }
136
137    #[test]
138    fn test_generate_json_schema_number() {
139        let input = json!(3.14);
140        let expected = json!({"type": "number"});
141        assert_eq!(generate_json_schema(&input), expected);
142    }
143
144    #[test]
145    fn test_generate_json_schema_boolean() {
146        let input = json!(true);
147        let expected = json!({"type": "boolean"});
148        assert_eq!(generate_json_schema(&input), expected);
149    }
150
151    #[test]
152    fn test_generate_json_schema_null() {
153        let input = json!(null);
154        let expected = json!({"type": "null"});
155        assert_eq!(generate_json_schema(&input), expected);
156    }
157
158    #[test]
159    fn test_generate_object_schema() {
160        let input = json!({
161            "name": "John Doe",
162            "age": 30,
163            "is_student": false
164        });
165        let expected = json!({
166            "$schema": "http://json-schema.org/draft-07/schema#",
167            "type": "object",
168            "properties": {
169                "name": {"type": "string"},
170                "age": {"type": "integer"},
171                "is_student": {"type": "boolean"}
172            },
173            "required": ["age", "is_student", "name"]
174        });
175        assert_eq!(generate_json_schema(&input), expected);
176    }
177
178    #[test]
179    fn test_generate_array_schema() {
180        let input = json!([1, 2, 3]);
181        let expected = json!({
182            "type": "array",
183            "items": {"type": "integer"}
184        });
185        assert_eq!(generate_json_schema(&input), expected);
186    }
187
188    #[test]
189    fn test_generate_array_schema_mixed_types() {
190        let input = json!([1, "two", 3.0]);
191        let expected = json!({
192            "type": "array",
193            "items": {
194                "oneOf": [
195                    {"oneOf": [
196                        {"type": "integer"},
197                        {"type": "string"}
198                    ]},
199                    {"type": "number"}
200                ]
201            }
202        });
203        assert_eq!(generate_json_schema(&input), expected);
204    }
205
206    #[test]
207    fn test_generate_array_schema_empty() {
208        let input = json!([]);
209        let expected = json!({
210            "type": "array",
211            "items": {}
212        });
213        assert_eq!(generate_json_schema(&input), expected);
214    }
215
216    #[test]
217    fn test_find_common_schema() {
218        let schemas = vec![
219            json!({"type": "integer"}),
220            json!({"type": "string"}),
221            json!({"type": "boolean"}),
222        ];
223        let expected = json!({
224            "oneOf": [
225                {"oneOf": [
226                    {"type": "integer"},
227                    {"type": "string"}
228                ]},
229                {"type": "boolean"}
230            ]
231        });
232        assert_eq!(find_common_schema(&schemas), expected);
233    }
234
235    #[test]
236    fn test_merge_schemas_same_type() {
237        let schema1 = json!({"type": "object", "properties": {"a": {"type": "string"}}});
238        let schema2 = json!({"type": "object", "properties": {"b": {"type": "integer"}}});
239        let expected = json!({
240            "type": "object",
241            "properties": {
242                "a": {"type": "string"},
243                "b": {"type": "integer"}
244            }
245        });
246        assert_eq!(merge_schemas(&schema1, &schema2), expected);
247    }
248
249    #[test]
250    fn test_merge_schemas_different_types() {
251        let schema1 = json!({"type": "string"});
252        let schema2 = json!({"type": "integer"});
253        let expected = json!({
254            "oneOf": [
255                {"type": "string"},
256                {"type": "integer"}
257            ]
258        });
259        assert_eq!(merge_schemas(&schema1, &schema2), expected);
260    }
261
262    #[test]
263    fn test_generate_schema_with_ref() {
264        let input = json!({
265            "$ref": "#/definitions/address",
266            "address": {
267                "street": "123 Main St",
268                "city": "New York"
269            }
270        });
271        let expected = json!({
272            "$schema": "http://json-schema.org/draft-07/schema#",
273            "type": "object",
274            "properties": {
275                "address": {
276                    "type": "object",
277                    "properties": {
278                        "street": {"type": "string"},
279                        "city": {"type": "string"}
280                    },
281                    "required": ["city", "street"]
282                }
283            },
284            "required": ["address"],
285            "$ref": "#/definitions/address"
286        });
287        assert_eq!(generate_json_schema(&input), expected);
288    }
289}