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);
enumerate_constants(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 enumerate_constants(map: &mut Map<String, Value>) {
let Some(Value::Array(branches)) = map.get("oneOf") else {
return;
};
if map.get("type").is_some_and(|t| t != "string")
|| map.contains_key("enum")
|| map.contains_key("const")
{
return;
}
let mut values: Vec<&str> = Vec::with_capacity(branches.len());
let mut meanings: Vec<String> = Vec::new();
for branch in branches {
let Some(branch) = branch.as_object() else {
return;
};
let only_annotated = branch
.keys()
.all(|key| matches!(key.as_str(), "type" | "const" | "enum" | "description"));
if !only_annotated || branch.get("type").and_then(Value::as_str) != Some("string") {
return;
}
let named: Vec<&str> = match (branch.get("const"), branch.get("enum")) {
(Some(Value::String(one)), None) => vec![one.as_str()],
(None, Some(Value::Array(many))) => {
let Some(many) = many.iter().map(Value::as_str).collect::<Option<Vec<_>>>() else {
return;
};
many
}
_ => return,
};
for value in &named {
if values.contains(value) {
return;
}
values.push(value);
}
match branch.get("description") {
Some(Value::String(meaning)) => {
let named: Vec<String> = named.iter().map(|v| format!("`{v}`")).collect();
meanings.push(format!("{}: {meaning}", named.join(", ")));
}
Some(_) => return,
None => {}
}
}
if values.is_empty() {
return;
}
let values: Vec<Value> = values.into_iter().map(Value::from).collect();
map.remove("oneOf");
map.insert("type".to_owned(), json!("string"));
map.insert("enum".to_owned(), Value::Array(values));
if !meanings.is_empty() {
let own = map.get("description").and_then(Value::as_str);
let text = own.map_or_else(
|| meanings.join("\n"),
|own| format!("{own}\n\n{}", meanings.join("\n")),
);
map.insert("description".to_owned(), Value::String(text));
}
}
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);
}
}
}