use serde_json::{Map, Value};
use tracing::warn;
pub fn transform_schema(schema: &str) -> String {
let mut value: Value = match serde_json::from_str(schema) {
Ok(v) => v,
Err(e) => {
warn!(error = %e, "failed to parse JSON schema for transformation, using original");
return schema.to_string();
}
};
let defs = extract_defs(&value);
remove_meta_fields(&mut value);
inline_refs(&mut value, &defs);
add_additional_properties_false(&mut value);
serde_json::to_string(&value).unwrap_or_else(|_| schema.to_string())
}
fn extract_defs(schema: &Value) -> Map<String, Value> {
schema
.get("$defs")
.or_else(|| schema.get("definitions"))
.and_then(|v| v.as_object())
.cloned()
.unwrap_or_default()
}
fn remove_meta_fields(value: &mut Value) {
if let Some(obj) = value.as_object_mut() {
obj.remove("$schema");
obj.remove("title");
obj.remove("default");
obj.remove("$defs");
obj.remove("definitions");
for (_, v) in obj.iter_mut() {
remove_meta_fields(v);
}
} else if let Some(arr) = value.as_array_mut() {
for item in arr.iter_mut() {
remove_meta_fields(item);
}
}
}
fn inline_refs(value: &mut Value, defs: &Map<String, Value>) {
match value {
Value::Object(obj) => {
if let Some(ref_val) = obj.get("$ref").and_then(|v| v.as_str()).map(String::from)
&& let Some(resolved) = resolve_ref(&ref_val, defs)
{
let mut resolved = resolved.clone();
inline_refs(&mut resolved, defs);
*value = resolved;
return;
}
for (_, v) in obj.iter_mut() {
inline_refs(v, defs);
}
}
Value::Array(arr) => {
for item in arr.iter_mut() {
inline_refs(item, defs);
}
}
_ => {}
}
}
fn resolve_ref<'a>(ref_path: &str, defs: &'a Map<String, Value>) -> Option<&'a Value> {
let name = ref_path
.strip_prefix("#/$defs/")
.or_else(|| ref_path.strip_prefix("#/definitions/"))?;
defs.get(name)
}
fn add_additional_properties_false(value: &mut Value) {
if let Some(obj) = value.as_object_mut() {
let is_object_schema = obj.get("type").and_then(|t| t.as_str()) == Some("object");
if is_object_schema && !obj.contains_key("additionalProperties") {
obj.insert("additionalProperties".to_string(), Value::Bool(false));
}
for (_, v) in obj.iter_mut() {
add_additional_properties_false(v);
}
} else if let Some(arr) = value.as_array_mut() {
for item in arr.iter_mut() {
add_additional_properties_false(item);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn adds_additional_properties_to_simple_object() {
let schema = r#"{"type":"object","properties":{"name":{"type":"string"}}}"#;
let result = transform_schema(schema);
let parsed: Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["additionalProperties"], json!(false));
}
#[test]
fn adds_additional_properties_to_nested_objects() {
let schema = json!({
"type": "object",
"properties": {
"address": {
"type": "object",
"properties": {
"city": {"type": "string"}
}
}
}
});
let result = transform_schema(&schema.to_string());
let parsed: Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["additionalProperties"], json!(false));
assert_eq!(
parsed["properties"]["address"]["additionalProperties"],
json!(false)
);
}
#[test]
fn preserves_existing_additional_properties() {
let schema = json!({
"type": "object",
"properties": {"x": {"type": "integer"}},
"additionalProperties": true
});
let result = transform_schema(&schema.to_string());
let parsed: Value = serde_json::from_str(&result).unwrap();
assert_eq!(parsed["additionalProperties"], json!(true));
}
#[test]
fn inlines_defs_refs() {
let schema = json!({
"type": "object",
"properties": {
"item": {"$ref": "#/$defs/Item"}
},
"$defs": {
"Item": {
"type": "object",
"properties": {
"name": {"type": "string"}
}
}
}
});
let result = transform_schema(&schema.to_string());
let parsed: Value = serde_json::from_str(&result).unwrap();
assert!(parsed.get("$defs").is_none());
assert_eq!(parsed["properties"]["item"]["type"], json!("object"));
assert_eq!(
parsed["properties"]["item"]["properties"]["name"]["type"],
json!("string")
);
assert_eq!(
parsed["properties"]["item"]["additionalProperties"],
json!(false)
);
}
#[test]
fn inlines_definitions_refs() {
let schema = json!({
"type": "object",
"properties": {
"item": {"$ref": "#/definitions/Item"}
},
"definitions": {
"Item": {
"type": "object",
"properties": {
"id": {"type": "integer"}
}
}
}
});
let result = transform_schema(&schema.to_string());
let parsed: Value = serde_json::from_str(&result).unwrap();
assert!(parsed.get("definitions").is_none());
assert_eq!(parsed["properties"]["item"]["type"], json!("object"));
}
#[test]
fn removes_meta_fields() {
let schema = json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "MySchema",
"type": "object",
"properties": {
"score": {
"type": "integer",
"title": "The Score",
"default": 0
}
}
});
let result = transform_schema(&schema.to_string());
let parsed: Value = serde_json::from_str(&result).unwrap();
assert!(parsed.get("$schema").is_none());
assert!(parsed.get("title").is_none());
assert!(parsed["properties"]["score"].get("title").is_none());
assert!(parsed["properties"]["score"].get("default").is_none());
}
#[test]
fn idempotent_on_already_transformed_schema() {
let schema = json!({
"type": "object",
"properties": {
"x": {"type": "integer"}
},
"additionalProperties": false
});
let input = schema.to_string();
let first = transform_schema(&input);
let second = transform_schema(&first);
assert_eq!(first, second);
}
#[test]
fn handles_invalid_json_gracefully() {
let bad = "not valid json{";
let result = transform_schema(bad);
assert_eq!(result, bad);
}
#[test]
fn handles_non_object_schema() {
let schema = r#"{"type":"string"}"#;
let result = transform_schema(schema);
let parsed: Value = serde_json::from_str(&result).unwrap();
assert!(parsed.get("additionalProperties").is_none());
}
#[test]
fn inlines_nested_refs() {
let schema = json!({
"type": "object",
"properties": {
"items": {
"type": "array",
"items": {"$ref": "#/$defs/Item"}
}
},
"$defs": {
"Item": {
"type": "object",
"properties": {
"nested": {"$ref": "#/$defs/Nested"}
}
},
"Nested": {
"type": "object",
"properties": {
"value": {"type": "string"}
}
}
}
});
let result = transform_schema(&schema.to_string());
let parsed: Value = serde_json::from_str(&result).unwrap();
let item = &parsed["properties"]["items"]["items"];
assert_eq!(item["type"], json!("object"));
assert_eq!(item["additionalProperties"], json!(false));
let nested = &item["properties"]["nested"];
assert_eq!(nested["type"], json!("object"));
assert_eq!(nested["additionalProperties"], json!(false));
assert_eq!(nested["properties"]["value"]["type"], json!("string"));
}
#[test]
fn handles_schemars_generated_schema() {
let schema = json!({
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Review",
"type": "object",
"required": ["score", "summary"],
"properties": {
"score": {"type": "integer", "default": 5},
"summary": {"type": "string"}
}
});
let result = transform_schema(&schema.to_string());
let parsed: Value = serde_json::from_str(&result).unwrap();
assert!(parsed.get("$schema").is_none());
assert!(parsed.get("title").is_none());
assert!(parsed["properties"]["score"].get("default").is_none());
assert_eq!(parsed["additionalProperties"], json!(false));
assert_eq!(parsed["required"], json!(["score", "summary"]));
}
}