use schemars::generate::SchemaSettings;
use schemars::JsonSchema;
use serde_json::{Map, Value};
pub fn schema_for<T: JsonSchema>() -> Value {
let generator = SchemaSettings::draft2020_12()
.with(|s| {
s.inline_subschemas = true;
s.meta_schema = None;
})
.into_generator();
let mut v = generator.into_root_schema_for::<T>().to_value();
normalize(&mut v);
if let Some(o) = v.as_object_mut() {
o.remove("title");
o.remove("description");
}
v
}
pub fn normalize(v: &mut Value) {
let Some(o) = v.as_object_mut() else { return };
if let Some(Value::Array(types)) = o.get("type") {
let rest: Vec<Value> = types.iter().filter(|t| t.as_str() != Some("null")).cloned().collect();
if rest.len() == 1 {
o.insert("type".into(), rest[0].clone());
}
}
if let Some(Value::Array(any)) = o.get("anyOf") {
let rest: Vec<Value> = any.iter().filter(|s| s.get("type").and_then(Value::as_str) != Some("null")).cloned().collect();
if rest.len() == 1 {
o.remove("anyOf");
if let Value::Object(inner) = &rest[0] {
for (k, x) in inner {
o.entry(k.clone()).or_insert_with(|| x.clone());
}
}
}
}
if let Some(Value::Array(one)) = o.get("oneOf") {
let mut values = Vec::new();
let mut labels = Vec::new();
let mut plain = true;
for s in one {
let vs: Vec<Value> = match (s.get("const"), s.get("enum")) {
(Some(c), _) => vec![c.clone()],
(None, Some(Value::Array(e))) => e.clone(),
_ => {
plain = false;
break;
}
};
for x in vs {
let label = s.get("title").or(s.get("description")).and_then(Value::as_str).map(str::to_string);
labels.push(label.map(Value::String).unwrap_or_else(|| x.clone()));
values.push(x);
}
}
if plain {
o.remove("oneOf");
o.insert("type".into(), "string".into());
if labels != values {
o.insert("x-enum-labels".into(), Value::Array(labels));
}
o.insert("enum".into(), Value::Array(values));
}
}
if let Some(d) = o.get("description").and_then(Value::as_str) {
let unwrapped = d.split("\n\n").map(|p| p.split_whitespace().collect::<Vec<_>>().join(" ")).collect::<Vec<_>>().join("\n\n");
o.insert("description".into(), unwrapped.into());
}
if !o.contains_key("title") {
if let Some(d) = o.get("description").and_then(Value::as_str).map(str::to_string) {
match d.split_once("\n\n") {
Some((title, rest)) => {
o.insert("title".into(), title.trim().into());
o.insert("description".into(), rest.trim().into());
}
None if d.len() <= 60 && !d.trim_end().ends_with('.') => {
o.insert("title".into(), d.trim().into());
o.remove("description");
}
None => {}
}
}
}
if o.get("default").is_some_and(Value::is_null) {
o.remove("default");
}
if let Some(f) = o.get("format").and_then(Value::as_str) {
if f.starts_with("int") || f.starts_with("uint") || f == "float" || f == "double" {
o.remove("format");
}
}
if let Some(Value::Object(props)) = o.get("properties") {
let order: Vec<Value> = props.keys().map(|k| Value::String(k.clone())).collect();
o.insert("x-order".into(), Value::Array(order));
}
for key in ["properties", "items"] {
match o.get_mut(key) {
Some(Value::Object(props)) if key == "properties" => props.values_mut().for_each(normalize),
Some(items @ Value::Object(_)) => normalize(items),
_ => {}
}
}
if let Some(Value::Object(props)) = o.get_mut("properties") {
let marked: Vec<String> = props
.iter_mut()
.filter_map(|(k, f)| f.as_object_mut()?.remove("x-required").filter(|v| v == &Value::Bool(true)).map(|_| k.clone()))
.collect();
if !marked.is_empty() {
let req = o.entry("required").or_insert_with(|| Value::Array(vec![]));
if let Value::Array(r) = req {
for k in marked {
if !r.contains(&Value::String(k.clone())) {
r.push(Value::String(k));
}
}
}
}
}
if o.get("required").and_then(Value::as_array).is_some_and(|r| r.is_empty()) {
o.remove("required");
}
}
#[derive(Clone, Copy, Debug, Default, serde::Deserialize)]
pub struct NoConfig {}
impl JsonSchema for NoConfig {
fn schema_name() -> std::borrow::Cow<'static, str> {
"NoConfig".into()
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
let mut m = Map::new();
m.insert("type".into(), "object".into());
m.insert("properties".into(), Value::Object(Map::new()));
schemars::Schema::from(m)
}
}
pub(crate) fn is_empty(schema: &Value) -> bool {
schema.get("properties").and_then(Value::as_object).is_none_or(|p| p.is_empty())
}
#[cfg(test)]
mod tests {
use super::*;
use serde::{Deserialize, Serialize};
use serde_json::json;
#[derive(Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "camelCase", default)]
#[allow(dead_code)]
struct Settings {
server: String,
#[schemars(extend("x-secret" = true))]
api_key: Option<String>,
mode: Mode,
#[schemars(range(min = 1, max = 10))]
retries: u32,
}
#[derive(Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "lowercase")]
#[allow(dead_code)]
enum Mode {
#[default]
Fast,
Slow,
}
#[derive(Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "camelCase", default)]
#[allow(dead_code)]
struct Marked {
#[schemars(extend("x-required" = true))]
server: String,
streams: Vec<Stream>,
}
#[derive(Serialize, Deserialize, JsonSchema, Default)]
#[serde(rename_all = "camelCase", default)]
#[allow(dead_code)]
struct Stream {
#[schemars(extend("x-system" = true, "x-required" = true))]
short_name: String,
port: u16,
}
#[test]
fn required_and_system_fields_come_through() {
let s = schema_for::<Marked>();
assert_eq!(s["required"], json!(["server"]));
let item = &s["properties"]["streams"]["items"];
assert_eq!(item["properties"]["shortName"]["x-system"], true);
assert_eq!(item["required"], json!(["shortName"]));
assert!(item["properties"]["shortName"].get("x-required").is_none());
assert!(item.get("required").is_some() && s["properties"]["streams"].get("required").is_none());
}
#[test]
fn normalizes_what_schemars_makes() {
let s = schema_for::<Settings>();
let p = &s["properties"];
assert_eq!(p["server"]["title"], "Upload server");
assert_eq!(p["server"]["description"], "Where calls go.");
assert_eq!(p["apiKey"]["type"], "string");
assert_eq!(p["apiKey"]["title"], "API key");
assert_eq!(p["apiKey"]["x-secret"], true);
assert!(p["apiKey"].get("default").is_none());
assert_eq!(p["mode"]["enum"], json!(["fast", "slow"]));
assert_eq!(p["mode"]["x-enum-labels"], json!(["fast", "Slow and steady"]));
assert_eq!(p["mode"]["default"], "fast");
assert_eq!(p["retries"]["maximum"], 10);
assert!(p["retries"].get("format").is_none());
assert!(s.get("title").is_none());
assert_eq!(s["x-order"], json!(["server", "apiKey", "mode", "retries"]));
}
}