use serde_json::{Map, Value, json};
const COMPOSING: &[&str] = &[
"oneOf",
"allOf",
"not",
"if",
"then",
"else",
"dependentSchemas",
"unevaluatedProperties",
"unevaluatedItems",
"$dynamicRef",
"$recursiveRef",
];
const MODERN_DIALECTS: &[&str] = &[
"https://json-schema.org/draft/2020-12/schema",
"https://json-schema.org/draft/2019-09/schema",
"http://json-schema.org/draft-07/schema",
];
#[must_use]
pub fn closed_schema(schema: &Value) -> Value {
let mut closed = schema.clone();
if composes(schema) {
return closed;
}
if let Some(map) = closed.as_object_mut()
&& map
.get("$schema")
.and_then(Value::as_str)
.is_some_and(|dialect| MODERN_DIALECTS.contains(&dialect.trim_end_matches('#')))
{
map.remove("$schema");
}
close(&mut closed, Grounds::Narrowing);
closed
}
#[must_use]
pub fn strict_schema<T: schemars::JsonSchema>() -> Value {
let mut schema = serde_json::to_value(schemars::schema_for!(T))
.expect("a schemars-generated schema must serialize to JSON");
if let Some(map) = schema.as_object_mut() {
map.remove("$schema");
}
close(&mut schema, Grounds::Serde);
schema
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Grounds {
Narrowing,
Serde,
}
fn close(node: &mut Value, grounds: Grounds) {
let Some(map) = node.as_object_mut() else {
return;
};
if grounds == Grounds::Serde {
drop_type_bounds(map);
}
if map.contains_key("$ref") && map.len() > 1 {
let target = map.remove("$ref").expect("checked above");
map.insert("anyOf".to_owned(), json!([{ "$ref": target }]));
}
if names_object(map) {
if grounds == Grounds::Serde || map.contains_key("properties") {
map.entry("additionalProperties")
.or_insert_with(|| json!(false));
}
if map.get("additionalProperties") == Some(&json!(false)) && !map.contains_key("properties")
{
map.insert("properties".to_owned(), json!({}));
map.entry("required").or_insert_with(|| json!([]));
}
if grounds == Grounds::Serde {
require_nullable(map);
}
}
for key in ["properties", "$defs", "definitions"] {
if let Some(children) = node.get_mut(key).and_then(Value::as_object_mut) {
for child in children.values_mut() {
close(child, grounds);
}
}
}
if let Some(items) = node.get_mut("items") {
close(items, grounds);
}
if let Some(branches) = node.get_mut("anyOf").and_then(Value::as_array_mut) {
for branch in branches {
close(branch, grounds);
}
}
}
fn names_object(map: &Map<String, Value>) -> bool {
match map.get("type") {
Some(Value::String(t)) => t == "object",
Some(Value::Array(types)) => types.iter().any(|t| t == "object"),
_ => false,
}
}
fn composes(node: &Value) -> bool {
let Some(map) = node.as_object() else {
return false;
};
if COMPOSING.iter().any(|key| map.contains_key(*key)) {
return true;
}
let tables = ["properties", "patternProperties", "$defs", "definitions"]
.iter()
.filter_map(|key| map.get(*key).and_then(Value::as_object))
.flat_map(Map::values);
let lists = ["anyOf", "prefixItems", "items"]
.iter()
.filter_map(|key| map.get(*key).and_then(Value::as_array))
.flatten();
let single = ["items", "additionalProperties", "contains", "propertyNames"]
.iter()
.filter_map(|key| map.get(*key));
tables.chain(lists).chain(single).any(composes)
}
fn require_nullable(map: &mut Map<String, Value>) {
let mut nullable: Vec<String> = map
.get("properties")
.and_then(Value::as_object)
.map(|properties| {
properties
.iter()
.filter(|(_, schema)| admits_null(schema))
.map(|(name, _)| name.clone())
.collect()
})
.unwrap_or_default();
if nullable.is_empty() {
return;
}
nullable.sort_unstable();
let required = map
.entry("required")
.or_insert_with(|| Value::Array(Vec::new()));
if let Some(required) = required.as_array_mut() {
for name in nullable {
if !required.iter().any(|r| r == name.as_str()) {
required.push(Value::String(name));
}
}
}
}
fn admits_null(schema: &Value) -> bool {
let null = json!("null");
match schema.get("type") {
Some(Value::Array(types)) if types.contains(&null) => return true,
_ => {}
}
schema
.get("anyOf")
.and_then(Value::as_array)
.is_some_and(|branches| branches.iter().any(|b| b.get("type") == Some(&null)))
}
const NUMBER_FORMATS: &[(&str, Option<i64>, Option<i64>)] = &[
("int8", Some(-128), Some(127)),
("int16", Some(-32_768), Some(32_767)),
("int32", None, None),
("int64", None, None),
("int128", None, None),
("int", None, None),
("uint8", Some(0), Some(255)),
("uint16", Some(0), Some(65_535)),
("uint32", Some(0), None),
("uint64", Some(0), None),
("uint128", Some(0), None),
("uint", Some(0), None),
("float", None, None),
("double", None, None),
];
fn drop_type_bounds(map: &mut Map<String, Value>) {
let Some((_, min, max)) = map
.get("format")
.and_then(Value::as_str)
.and_then(|format| NUMBER_FORMATS.iter().find(|(name, ..)| *name == format))
else {
return;
};
map.remove("format");
for (key, bound) in [("minimum", min), ("maximum", max)] {
if bound.is_some() && map.get(key).and_then(Value::as_i64) == *bound {
map.remove(key);
}
}
}