use serde_json::{Map, Value};
use std::collections::HashSet;
pub(super) fn to_openapi_schema(schema: &mut Value) {
if let Value::Object(map) = schema {
let defs = extract_defs(map);
if !defs.is_empty() {
let mut visited = HashSet::new();
resolve_refs(map, &defs, &mut visited);
}
simplify_object(map);
}
}
fn extract_defs(map: &mut Map<String, Value>) -> Map<String, Value> {
let mut defs = Map::new();
for key in &["$defs", "definitions"] {
if let Some(Value::Object(d)) = map.remove(*key) {
defs.extend(d);
}
}
defs
}
fn resolve_refs(map: &mut Map<String, Value>, defs: &Map<String, Value>, visited: &mut HashSet<String>) {
if let Some(Value::String(ref_path)) = map.get("$ref") {
let name = ref_path.rsplit('/').next().unwrap_or("").to_string();
if !name.is_empty()
&& !visited.contains(&name)
&& let Some(def) = defs.get(&name)
{
visited.insert(name.clone());
let mut resolved = def.clone();
if let Value::Object(ref mut inner) = resolved {
resolve_refs(inner, defs, visited);
}
visited.remove(&name);
map.remove("$ref");
if let Value::Object(inner) = resolved {
for (k, v) in inner {
map.entry(&k).or_insert(v);
}
}
return;
}
}
for value in map.values_mut() {
match value {
Value::Object(child) => resolve_refs(child, defs, visited),
Value::Array(arr) => {
for item in arr.iter_mut() {
if let Value::Object(child) = item {
resolve_refs(child, defs, visited);
}
}
}
_ => {}
}
}
}
fn simplify_object(map: &mut Map<String, Value>) {
flatten_composites(map);
normalize_nullable_type(map);
rewrite_const_as_single_enum(map);
strip_unsupported_json_schema_keywords(map);
recurse_into_children(map);
}
fn flatten_composites(map: &mut Map<String, Value>) {
for keyword in &["allOf", "anyOf", "oneOf"] {
let Some(Value::Array(variants)) = map.remove(*keyword) else {
continue;
};
let non_null: Vec<Value> = variants.into_iter().filter(|v| !is_null_schema(v)).collect();
if non_null.len() == 1 {
if let Value::Object(inner) = non_null.into_iter().next().unwrap() {
for (k, v) in inner {
map.entry(&k).or_insert(v);
}
}
} else if !non_null.is_empty() {
map.insert(keyword.to_string(), Value::Array(non_null));
}
if !map.contains_key(*keyword) {
continue;
}
}
}
fn is_null_schema(v: &Value) -> bool {
match v {
Value::Object(m) => m.get("type").and_then(Value::as_str) == Some("null"),
_ => false,
}
}
fn normalize_nullable_type(map: &mut Map<String, Value>) {
if let Some(Value::Array(types)) = map.get("type") {
let non_null: Vec<&Value> = types.iter().filter(|t| t.as_str() != Some("null")).collect();
if non_null.len() == 1 {
let single = non_null[0].clone();
map.insert("type".to_string(), single);
}
}
}
fn rewrite_const_as_single_enum(map: &mut Map<String, Value>) {
if let Some(const_value) = map.remove("const") {
map.insert("enum".to_string(), Value::Array(vec![const_value]));
}
}
fn strip_unsupported_json_schema_keywords(map: &mut Map<String, Value>) {
for key in [
"$schema",
"$id",
"$anchor",
"$dynamicAnchor",
"$dynamicRef",
"$ref",
"patternProperties",
"unevaluatedProperties",
"unevaluatedItems",
"propertyNames",
"contains",
"minContains",
"maxContains",
"if",
"then",
"else",
"dependentSchemas",
"dependentRequired",
"not",
] {
map.remove(key);
}
}
fn recurse_into_children(map: &mut Map<String, Value>) {
if let Some(Value::Object(props)) = map.get_mut("properties") {
for prop_schema in props.values_mut() {
if let Value::Object(inner) = prop_schema {
simplify_object(inner);
}
}
}
if let Some(items) = map.get_mut("items") {
match items {
Value::Object(inner) => simplify_object(inner),
Value::Array(arr) => {
for item in arr.iter_mut() {
if let Value::Object(inner) = item {
simplify_object(inner);
}
}
}
_ => {}
}
}
if let Some(Value::Array(arr)) = map.get_mut("prefixItems") {
for item in arr.iter_mut() {
if let Value::Object(inner) = item {
simplify_object(inner);
}
}
}
if let Some(Value::Object(inner)) = map.get_mut("additionalProperties") {
simplify_object(inner);
}
for keyword in &["allOf", "anyOf", "oneOf"] {
if let Some(Value::Array(arr)) = map.get_mut(*keyword) {
for item in arr.iter_mut() {
if let Value::Object(inner) = item {
simplify_object(inner);
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_passthrough_clean_schema() {
let mut schema = json!({
"type": "object",
"properties": {
"name": { "type": "string" }
},
"required": ["name"]
});
let expected = json!({
"type": "object",
"properties": {
"name": { "type": "string" }
},
"required": ["name"]
});
to_openapi_schema(&mut schema);
assert_eq!(schema, expected);
}
#[test]
fn test_preserves_additional_properties() {
let mut schema = json!({
"type": "object",
"properties": {
"inner": {
"type": "object",
"additionalProperties": false,
"properties": {
"x": { "type": "integer", "additionalProperties": false }
}
}
},
"additionalProperties": false
});
to_openapi_schema(&mut schema);
assert_eq!(schema["additionalProperties"], false);
let inner = &schema["properties"]["inner"];
assert_eq!(inner["additionalProperties"], false);
let x = &inner["properties"]["x"];
assert_eq!(x["additionalProperties"], false);
}
#[test]
fn test_simplifies_additional_properties_schema() {
let mut schema = json!({
"type": "object",
"additionalProperties": {
"type": ["string", "null"],
"const": "ok"
}
});
to_openapi_schema(&mut schema);
assert_eq!(schema["additionalProperties"]["type"], "string");
assert_eq!(schema["additionalProperties"]["enum"], json!(["ok"]));
}
#[test]
fn test_const_rewritten_as_single_value_enum() {
let mut schema = json!({
"type": "object",
"properties": {
"target": {
"oneOf": [
{
"type": "object",
"properties": {
"relation": { "const": "self" }
},
"required": ["relation"]
},
{
"type": "object",
"properties": {
"relation": {
"type": "string",
"const": "child",
"enum": ["child", "sibling"]
},
"name": { "type": "string" }
},
"required": ["relation", "name"]
}
]
}
},
"required": ["target"]
});
to_openapi_schema(&mut schema);
let serialized = serde_json::to_string(&schema).unwrap();
assert!(!serialized.contains("\"const\""));
assert_eq!(
schema["properties"]["target"]["oneOf"][0]["properties"]["relation"]["enum"],
json!(["self"])
);
assert_eq!(
schema["properties"]["target"]["oneOf"][1]["properties"]["relation"]["enum"],
json!(["child"])
);
}
#[test]
fn test_nullable_array_type() {
let mut schema = json!({
"type": "object",
"properties": {
"name": { "type": ["string", "null"] }
}
});
to_openapi_schema(&mut schema);
assert_eq!(schema["properties"]["name"]["type"], "string");
}
#[test]
fn test_nullable_array_type_null_first() {
let mut schema = json!({
"type": "object",
"properties": {
"count": { "type": ["null", "integer"] }
}
});
to_openapi_schema(&mut schema);
assert_eq!(schema["properties"]["count"]["type"], "integer");
}
#[test]
fn test_single_allof_flattening() {
let mut schema = json!({
"type": "object",
"properties": {
"config": {
"allOf": [
{ "type": "object", "properties": { "key": { "type": "string" } } }
]
}
}
});
to_openapi_schema(&mut schema);
assert_eq!(schema["properties"]["config"]["type"], "object");
assert_eq!(schema["properties"]["config"]["properties"]["key"]["type"], "string");
assert!(schema["properties"]["config"].get("allOf").is_none());
}
#[test]
fn test_nullable_anyof_schemars_pattern() {
let mut schema = json!({
"type": "object",
"properties": {
"label": {
"anyOf": [
{ "type": "string" },
{ "type": "null" }
]
}
}
});
to_openapi_schema(&mut schema);
assert_eq!(schema["properties"]["label"]["type"], "string");
assert!(schema["properties"]["label"].get("anyOf").is_none());
}
#[test]
fn test_nullable_oneof() {
let mut schema = json!({
"type": "object",
"properties": {
"value": {
"oneOf": [
{ "type": "integer" },
{ "type": "null" }
]
}
}
});
to_openapi_schema(&mut schema);
assert_eq!(schema["properties"]["value"]["type"], "integer");
assert!(schema["properties"]["value"].get("oneOf").is_none());
}
#[test]
fn test_ref_resolution_and_defs_removal() {
let mut schema = json!({
"type": "object",
"properties": {
"address": { "$ref": "#/$defs/Address" }
},
"$defs": {
"Address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"city": { "type": "string" }
},
"additionalProperties": false
}
}
});
to_openapi_schema(&mut schema);
assert!(schema.get("$defs").is_none());
assert!(schema["properties"]["address"].get("$ref").is_none());
assert_eq!(schema["properties"]["address"]["type"], "object");
assert_eq!(
schema["properties"]["address"]["properties"]["street"]["type"],
"string"
);
assert_eq!(schema["properties"]["address"]["additionalProperties"], false);
}
#[test]
fn test_ref_with_definitions_key() {
let mut schema = json!({
"type": "object",
"properties": {
"item": { "$ref": "#/definitions/Item" }
},
"definitions": {
"Item": {
"type": "object",
"properties": {
"id": { "type": "integer" }
}
}
}
});
to_openapi_schema(&mut schema);
assert!(schema.get("definitions").is_none());
assert_eq!(schema["properties"]["item"]["type"], "object");
}
#[test]
fn test_cycle_detection() {
let mut schema = json!({
"type": "object",
"properties": {
"node": { "$ref": "#/$defs/Node" }
},
"$defs": {
"Node": {
"type": "object",
"properties": {
"child": { "$ref": "#/$defs/Node" }
}
}
}
});
to_openapi_schema(&mut schema);
assert!(schema.get("$defs").is_none());
assert_eq!(schema["properties"]["node"]["type"], "object");
assert!(schema["properties"]["node"]["properties"]["child"].get("$ref").is_none());
}
#[test]
fn test_complex_schemars_schema() {
let mut schema = json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"name": { "type": "string" },
"age": {
"anyOf": [
{ "type": "integer" },
{ "type": "null" }
]
},
"address": {
"anyOf": [
{ "$ref": "#/$defs/Address" },
{ "type": "null" }
]
}
},
"required": ["name"],
"additionalProperties": false,
"$defs": {
"Address": {
"type": "object",
"properties": {
"street": { "type": "string" },
"zip": { "type": ["string", "null"] }
},
"required": ["street"],
"additionalProperties": false
}
}
});
to_openapi_schema(&mut schema);
assert!(schema.get("$defs").is_none());
assert_eq!(schema["additionalProperties"], false);
assert_eq!(schema["properties"]["age"]["type"], "integer");
assert!(schema["properties"]["age"].get("anyOf").is_none());
assert_eq!(schema["properties"]["address"]["type"], "object");
assert!(schema["properties"]["address"].get("anyOf").is_none());
assert!(schema["properties"]["address"].get("$ref").is_none());
assert_eq!(schema["properties"]["address"]["additionalProperties"], false);
assert_eq!(schema["properties"]["address"]["properties"]["zip"]["type"], "string");
}
#[test]
fn test_nested_recursive_simplification() {
let mut schema = json!({
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {
"type": "object",
"properties": {
"value": { "type": ["number", "null"] }
},
"additionalProperties": false
}
}
}
});
to_openapi_schema(&mut schema);
let items_schema = &schema["properties"]["items"]["items"];
assert_eq!(items_schema["properties"]["value"]["type"], "number");
assert_eq!(items_schema["additionalProperties"], false);
}
#[test]
fn test_multi_variant_composite_preserved() {
let mut schema = json!({
"type": "object",
"properties": {
"data": {
"oneOf": [
{ "type": "string" },
{ "type": "integer" }
]
}
}
});
to_openapi_schema(&mut schema);
assert!(schema["properties"]["data"].get("oneOf").is_some());
let variants = schema["properties"]["data"]["oneOf"].as_array().unwrap();
assert_eq!(variants.len(), 2);
}
#[test]
fn test_non_object_schema_passthrough() {
let mut schema = json!("string");
to_openapi_schema(&mut schema);
assert_eq!(schema, json!("string"));
}
#[test]
fn test_nested_ref_chain() {
let mut schema = json!({
"type": "object",
"properties": {
"a": { "$ref": "#/$defs/A" }
},
"$defs": {
"A": {
"type": "object",
"properties": {
"b": { "$ref": "#/$defs/B" }
}
},
"B": {
"type": "object",
"properties": {
"value": { "type": "string" }
},
"additionalProperties": false
}
}
});
to_openapi_schema(&mut schema);
assert!(schema.get("$defs").is_none());
assert_eq!(schema["properties"]["a"]["type"], "object");
assert_eq!(schema["properties"]["a"]["properties"]["b"]["type"], "object");
assert_eq!(
schema["properties"]["a"]["properties"]["b"]["properties"]["value"]["type"],
"string"
);
assert_eq!(
schema["properties"]["a"]["properties"]["b"]["additionalProperties"],
false
);
}
#[test]
fn test_strips_unsupported_json_schema_keywords() {
let mut schema = json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"patternProperties": {
"^x_": { "type": "string" }
},
"properties": {
"name": {
"type": "string",
"not": { "enum": ["bad"] }
}
},
"if": { "properties": { "kind": { "const": "a" } } },
"then": { "required": ["name"] }
});
to_openapi_schema(&mut schema);
let serialized = serde_json::to_string(&schema).unwrap();
for keyword in ["\"$schema\"", "\"patternProperties\"", "\"not\"", "\"if\"", "\"then\""] {
assert!(
!serialized.contains(keyword),
"schema still contains {keyword}: {serialized}"
);
}
}
}