//! Integration test: public API for schema parsing and code generation, and CLI binary.
use json_schema_rs::{
CodeGenSettings, DedupeMode, JsonSchema, JsonSchemaSettings, ModelNameSource, SpecVersion,
generate_rust, resolved_spec_version,
};
use std::collections::{BTreeMap, BTreeSet};
use std::fs;
use std::io::Write;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::process::Stdio;
fn jsonschemars_bin() -> std::path::PathBuf {
if let Ok(path) = std::env::var("CARGO_BIN_EXE_jsonschemars") {
return std::path::PathBuf::from(path);
}
let manifest_dir =
std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR set by Cargo");
let target_dir = std::env::var("CARGO_TARGET_DIR").unwrap_or_else(|_| {
std::path::PathBuf::from(&manifest_dir)
.join("..")
.join("target")
.into_os_string()
.into_string()
.unwrap()
});
let bin = std::path::Path::new(&target_dir)
.join("debug")
.join("jsonschemars");
assert!(
bin.exists(),
"jsonschemars binary not found at {}",
bin.display()
);
bin
}
#[test]
fn integration_schema_with_deprecated_property_parses_and_generates() {
let schema_json =
r#"{"type":"object","properties":{"legacy":{"type":"string","deprecated":true}}}"#;
let schema: JsonSchema = JsonSchema::try_from(schema_json).expect("parse schema");
let settings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &settings).expect("generate");
let actual: String = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
#[deprecated]
pub legacy: Option<String>,
}
";
assert_eq!(expected, actual);
}
#[test]
fn integration_schema_with_default_parses_and_generates() {
let schema_json =
r#"{"type":"object","properties":{"name":{"type":"string","default":"foo"}}}"#;
let schema: JsonSchema = JsonSchema::try_from(schema_json).expect("parse schema");
let settings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &settings).expect("generate");
let actual: String = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
fn default_root_name() -> Option<String> { Some("foo".to_string()) }
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
#[serde(default = "default_root_name")]
pub name: Option<String>,
}
"#;
assert_eq!(expected, actual);
}
#[test]
fn integration_schema_keyword_parse_round_trip_and_resolved_spec_version() {
let schema_json = r#"{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object","properties":{"name":{"type":"string"}}}"#;
let settings: JsonSchemaSettings = JsonSchemaSettings::default();
let schema: JsonSchema = JsonSchema::try_from(schema_json).expect("parse");
let expected_uri: Option<String> =
Some("https://json-schema.org/draft/2020-12/schema".to_string());
assert_eq!(expected_uri, schema.schema);
let expected_version: SpecVersion = SpecVersion::Draft202012;
let actual_version: SpecVersion = resolved_spec_version(&schema, &settings);
assert_eq!(expected_version, actual_version);
let serialized: String = (&schema).try_into().expect("serialize");
assert!(
serialized.contains("\"$schema\""),
"serialized schema must contain $schema key"
);
assert!(
serialized.contains("2020-12"),
"serialized schema must contain 2020-12 URI"
);
let reparsed: JsonSchema = JsonSchema::try_from(serialized.as_str()).expect("parse again");
assert_eq!(schema.schema, reparsed.schema);
}
#[test]
fn cli_generate_rust_single_file_to_output_dir() {
let schema_json =
r#"{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub id: String,
}
";
assert_eq!(expected, actual);
}
#[test]
fn cli_generate_rust_defs_ref() {
let schema_json = r##"{
"$defs": {
"Address": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
},
"type": "object",
"properties": { "address": { "$ref": "#/$defs/Address" } },
"required": ["address"]
}"##;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Address {
pub city: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub address: Address,
}
";
assert_eq!(expected, actual);
}
#[test]
fn cli_generate_rust_enum_property() {
let schema_json = r#"{"type":"object","properties":{"status":{"enum":["open","closed"]}},"required":["status"]}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
assert!(actual.contains("pub enum Status"));
assert!(actual.contains("pub struct Root"));
assert!(actual.contains("pub status: Status"));
assert!(actual.contains("Open"));
assert!(actual.contains("Closed"));
}
#[test]
fn cli_generate_rust_enum_collision() {
let schema_json = r#"{"type":"object","properties":{"t":{"enum":["a","A"]}},"required":["t"]}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
assert!(actual.contains("pub enum T"));
assert!(actual.contains("A0"));
assert!(actual.contains("A1"));
assert!(actual.contains("pub t: T"));
}
#[test]
fn cli_generate_rust_enum_dedupe() {
let schema_json = r#"{"type":"object","properties":{"a":{"enum":["x","y"]},"b":{"enum":["x","y"]}},"required":["a"]}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
assert!(actual.contains("pub enum A"));
assert!(actual.contains("pub a: A"));
assert!(actual.contains("pub b: Option<A>"));
}
#[test]
fn cli_generate_rust_enum_duplicate_values() {
let schema_json = r#"{"type":"object","properties":{"t":{"enum":["A","A","A","a","a","a","a","a","a","a"]}},"required":["t"]}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
assert!(actual.contains("pub enum T"));
assert!(actual.contains("A0"));
assert!(actual.contains("A1"));
assert!(actual.contains("pub t: T"));
}
#[test]
fn cli_generate_rust_enum_non_rust_compliant() {
let schema_json = r#"{"type":"object","properties":{"id":{"enum":["/8633","todd.griffin"]}},"required":["id"]}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
assert!(actual.contains("pub enum Id"));
assert!(actual.contains("E8633"));
assert!(actual.contains("ToddGriffin"));
assert!(actual.contains("pub id: Id"));
}
#[test]
fn cli_generate_rust_integer_property() {
let schema_json =
r#"{"type":"object","properties":{"count":{"type":"integer"}},"required":["count"]}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub count: i64,
}
";
assert_eq!(expected, actual);
}
#[test]
fn cli_generate_rust_optional_integer() {
let schema_json = r#"{"type":"object","properties":{"count":{"type":"integer"}}}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub count: Option<i64>,
}
";
assert_eq!(expected, actual);
}
#[test]
fn cli_generate_rust_integer_with_min_max_emits_u8() {
let schema_json = r#"{"type":"object","properties":{"byte":{"type":"integer","minimum":0,"maximum":255}},"required":["byte"]}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub byte: u8,
}
";
assert_eq!(expected, actual);
}
#[test]
fn cli_generate_rust_number_with_min_max_emits_f32() {
let schema_json = r#"{"type":"object","properties":{"value":{"type":"number","minimum":0.5,"maximum":100.5}},"required":["value"]}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub value: f32,
}
";
assert_eq!(expected, actual);
}
#[test]
fn cli_generate_rust_float_property() {
let schema_json =
r#"{"type":"object","properties":{"value":{"type":"number"}},"required":["value"]}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub value: f64,
}
";
assert_eq!(expected, actual);
}
#[test]
fn cli_generate_rust_optional_float() {
let schema_json = r#"{"type":"object","properties":{"value":{"type":"number"}}}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub value: Option<f64>,
}
";
assert_eq!(expected, actual);
}
#[test]
fn cli_generate_rust_boolean_property() {
let schema_json = r#"{"type":"object","properties":{"enabled":{"type":"boolean"},"flag":{"type":"boolean"}},"required":["enabled"]}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub enabled: bool,
pub flag: Option<bool>,
}
";
assert_eq!(expected, actual);
}
#[test]
fn cli_generate_rust_array_required() {
let schema_json = r#"{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"}}},"required":["tags"]}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub tags: Vec<String>,
}
";
assert_eq!(expected, actual);
}
#[test]
fn cli_generate_rust_array_optional() {
let schema_json =
r#"{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"}}}}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
assert!(actual.contains("pub tags: Option<Vec<String>>"));
}
#[test]
fn cli_generate_rust_array_unique_items_true() {
let schema_json = r#"{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"},"uniqueItems":true}},"required":["tags"]}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
assert!(
actual.contains("pub tags: HashSet<String>"),
"expected HashSet<String>: {actual}"
);
assert!(
actual.contains("use std::collections::HashSet"),
"expected HashSet use: {actual}"
);
}
#[test]
fn cli_generate_rust_array_min_items_max_items() {
let schema_json = r#"{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"},"minItems":2,"maxItems":5}},"required":["tags"]}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
assert!(
actual.contains("#[json_schema(min_items = 2, max_items = 5)]"),
"expected min_items/max_items attribute: {actual}"
);
assert!(actual.contains("pub tags: Vec<String>"));
}
#[test]
fn cli_generate_rust_string_min_length_max_length() {
let schema_json = r#"{"type":"object","properties":{"name":{"type":"string","minLength":2,"maxLength":50}},"required":["name"]}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
assert!(
actual.contains("#[json_schema(min_length = 2, max_length = 50)]"),
"expected min_length/max_length attribute: {actual}"
);
assert!(actual.contains("pub name: String"));
}
#[test]
fn cli_generate_rust_string_pattern() {
let schema_json = r#"{"type":"object","properties":{"name":{"type":"string","pattern":"^[a-z]+$"}},"required":["name"]}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
let out_path = out_dir.path().join("schema.rs");
let actual: (bool, String) = (
output.status.success(),
std::fs::read_to_string(&out_path).unwrap_or_default(),
);
let expected: (bool, String) = (
true,
concat!(
"//! Generated by json-schema-rs. Do not edit manually.\n\n",
"use serde::{Deserialize, Serialize};\n\n",
"#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\n",
"pub struct Root {\n",
" #[json_schema(pattern = \"^[a-z]+$\")]\n",
" pub name: String,\n",
"}\n\n"
)
.to_string(),
);
assert_eq!(expected, actual);
}
#[test]
fn cli_generate_unsupported_language_exit_nonzero_and_stderr() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("x.json");
std::fs::write(
&schema_path,
r#"{"type":"object","properties":{"x":{"type":"string"}}}"#,
)
.expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"python",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(!output.status.success(), "generate python should fail");
let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
assert!(
stderr.contains("unsupported"),
"stderr should mention unsupported language: {stderr}"
);
}
#[test]
fn cli_generate_rust_multiple_files_mirror_paths() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let a_path = temp_dir.path().join("a.json");
let b_path = temp_dir.path().join("b.json");
std::fs::write(
&a_path,
r#"{"type":"object","properties":{"a":{"type":"string"}}}"#,
)
.expect("write a");
std::fs::write(
&b_path,
r#"{"type":"object","properties":{"b":{"type":"string"}}}"#,
)
.expect("write b");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
a_path.to_str().unwrap(),
b_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let a_rs = std::fs::read_to_string(out_dir.path().join("a.rs")).expect("read a.rs");
let b_rs = std::fs::read_to_string(out_dir.path().join("b.rs")).expect("read b.rs");
assert!(
a_rs.contains("pub a: Option<String>"),
"a.rs should have field a"
);
assert!(
b_rs.contains("pub b: Option<String>"),
"b.rs should have field b"
);
}
#[test]
fn cli_generate_rust_dir_recursive_mirrors_structure() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let nested = temp_dir.path().join("nested");
std::fs::create_dir_all(&nested).expect("create nested");
std::fs::write(
temp_dir.path().join("root.json"),
r#"{"type":"object","properties":{"x":{"type":"string"}}}"#,
)
.expect("write root");
std::fs::write(
nested.join("child.json"),
r#"{"type":"object","properties":{"y":{"type":"string"}}}"#,
)
.expect("write child");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
temp_dir.path().to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let root_rs = std::fs::read_to_string(out_dir.path().join("root.rs")).expect("read root.rs");
let child_rs = std::fs::read_to_string(out_dir.path().join("nested").join("child.rs"))
.expect("read nested/child.rs");
assert!(
root_rs.contains("pub x: Option<String>"),
"root.rs should have field x"
);
assert!(
child_rs.contains("pub y: Option<String>"),
"nested/child.rs should have field y"
);
let root_mod = std::fs::read_to_string(out_dir.path().join("mod.rs")).expect("read mod.rs");
let nested_mod = std::fs::read_to_string(out_dir.path().join("nested").join("mod.rs"))
.expect("read nested/mod.rs");
assert!(
root_mod.contains("pub mod nested;") && root_mod.contains("pub mod root;"),
"root mod.rs should re-export nested and root: {root_mod}"
);
assert!(
nested_mod.contains("pub mod child;"),
"nested/mod.rs should re-export child: {nested_mod}"
);
}
#[test]
fn cli_generate_rust_ingestion_single_failure_logs_path_and_exits_nonzero() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let bad_path = temp_dir.path().join("bad.json");
std::fs::write(&bad_path, r#"{"type":"object","properties":{}"#).expect("write invalid json");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
bad_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(!output.status.success(), "should exit non-zero");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("bad.json"),
"stderr should contain failing file path: {stderr}"
);
assert!(
stderr.contains("invalid JSON Schema") || stderr.contains("EOF"),
"stderr should contain error reason: {stderr}"
);
}
#[test]
fn cli_generate_rust_ingestion_multiple_failures_logs_all_and_exits_nonzero() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let bad1 = temp_dir.path().join("bad1.json");
let bad2 = temp_dir.path().join("bad2.json");
std::fs::write(&bad1, r#"{"type":"object"#).expect("write bad1");
std::fs::write(&bad2, r"not json").expect("write bad2");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
bad1.to_str().unwrap(),
bad2.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(!output.status.success(), "should exit non-zero");
let stderr = String::from_utf8_lossy(&output.stderr);
let expected_bad1 = stderr.contains("bad1.json");
let expected_bad2 = stderr.contains("bad2.json");
assert!(
expected_bad1 && expected_bad2,
"stderr should contain both failing file paths; stderr: {stderr}"
);
}
#[test]
fn cli_generate_rust_ingestion_partial_success_no_output_written() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let good_path = temp_dir.path().join("good.json");
let bad_path = temp_dir.path().join("bad.json");
std::fs::write(
&good_path,
r#"{"type":"object","properties":{"x":{"type":"string"}}}"#,
)
.expect("write good");
std::fs::write(&bad_path, r#"{"type":"object"#).expect("write bad");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
good_path.to_str().unwrap(),
bad_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(!output.status.success(), "should exit non-zero");
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("bad.json"),
"stderr should log failing file: {stderr}"
);
let good_rs = out_dir.path().join("good.rs");
assert!(
!good_rs.exists(),
"no output should be written when any ingestion fails: {good_rs:?} should not exist"
);
}
#[test]
fn cli_generate_rust_hyphenated_paths_sanitized_and_mod_rs_emitted() {
let temp_dir = tempfile::tempdir().expect("temp dir");
let sub_dir = temp_dir.path().join("sub-dir");
std::fs::create_dir_all(&sub_dir).expect("create sub-dir");
std::fs::write(
temp_dir.path().join("schema-1.json"),
r#"{"type":"object","properties":{"a":{"type":"string"}}}"#,
)
.expect("write schema-1");
std::fs::write(
sub_dir.join("schema-2.json"),
r#"{"type":"object","properties":{"b":{"type":"string"}}}"#,
)
.expect("write schema-2");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
temp_dir.path().to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let schema_1_rs = out_dir.path().join("schema_1.rs");
let sub_dir_schema_2 = out_dir.path().join("sub_dir").join("schema_2.rs");
assert!(
schema_1_rs.exists(),
"schema_1.rs should exist (sanitized from schema-1.json)"
);
assert!(
sub_dir_schema_2.exists(),
"sub_dir/schema_2.rs should exist (sanitized path)"
);
let root_mod = std::fs::read_to_string(out_dir.path().join("mod.rs")).expect("read mod.rs");
let expected_root =
root_mod.contains("pub mod schema_1;") && root_mod.contains("pub mod sub_dir;");
assert!(
expected_root,
"root mod.rs should declare schema_1 and sub_dir: {root_mod}"
);
let sub_dir_mod = std::fs::read_to_string(out_dir.path().join("sub_dir").join("mod.rs"))
.expect("read sub_dir/mod.rs");
assert!(
sub_dir_mod.contains("pub mod schema_2;"),
"sub_dir/mod.rs should declare schema_2: {sub_dir_mod}"
);
}
#[test]
fn cli_generate_rust_optional_string() {
let schema_json = r#"{"type":"object","properties":{"name":{"type":"string"}}}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub name: Option<String>,
}
";
assert_eq!(expected, actual);
}
#[test]
fn cli_generate_rust_description() {
let schema_json = r#"{"type":"object","description":"Root type","properties":{"name":{"type":"string","description":"User name"}}}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
/// Root type
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
/// User name
pub name: Option<String>,
}
";
assert_eq!(expected, actual);
}
#[test]
fn cli_generate_rust_nested_object() {
let schema_json = r#"{"type":"object","properties":{"address":{"type":"object","properties":{"city":{"type":"string"},"street_address":{"type":"string"}}}}}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
assert!(
actual.contains("pub struct Address"),
"output should contain Address struct: {actual}"
);
assert!(
actual.contains("pub address: Option<Address>"),
"output should contain address field: {actual}"
);
assert!(
actual.contains("pub city: Option<String>")
&& actual.contains("pub street_address: Option<String>"),
"output should contain nested fields: {actual}"
);
}
#[test]
fn cli_generate_rust_hyphenated_property_key() {
let schema_json = r#"{"type":"object","properties":{"foo-bar":{"type":"string"}}}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
#[serde(rename = "foo-bar")]
pub foo_bar: Option<String>,
}
"#;
assert_eq!(expected, actual);
}
#[test]
fn cli_generate_rust_dedupe_two_identical_schemas() {
let schema_json = r#"{"type":"object","properties":{"id":{"type":"string"}}}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let a_path = temp_dir.path().join("a.json");
let b_path = temp_dir.path().join("b.json");
std::fs::write(&a_path, schema_json).expect("write a");
std::fs::write(&b_path, schema_json).expect("write b");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
a_path.to_str().unwrap(),
b_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let a_rs_path = out_dir.path().join("a.rs");
assert!(
a_rs_path.exists(),
"a.rs should exist when two identical schemas"
);
let actual: String = std::fs::read_to_string(&a_rs_path).expect("read a.rs");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
pub use super::shared::Root;
";
assert_eq!(expected, actual);
}
#[test]
fn cli_generate_rust_dedupe_nested_output_uses_super_super_shared() {
let schema_json = r#"{"type":"object","properties":{"id":{"type":"string"}}}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let sub_dir = temp_dir.path().join("sub");
std::fs::create_dir_all(&sub_dir).expect("create sub");
let c_path = sub_dir.join("c.json");
let d_path = sub_dir.join("d.json");
std::fs::write(&c_path, schema_json).expect("write c");
std::fs::write(&d_path, schema_json).expect("write d");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
temp_dir.path().to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let c_rs_path = out_dir.path().join("sub").join("c.rs");
assert!(
c_rs_path.exists(),
"sub/c.rs should exist when schemas are in subdir"
);
let actual: String = std::fs::read_to_string(&c_rs_path).expect("read sub/c.rs");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
pub use super::super::shared::Root;
";
assert_eq!(expected, actual);
}
#[test]
fn cli_generate_rust_model_name_source_property_key() {
let schema_json = r#"{"type":"object","properties":{"address":{"type":"object","title":"FooBar","properties":{"city":{"type":"string"}}}}}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
"--cgs-model-name-source",
"property-key",
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
assert!(
actual.contains("pub struct Address "),
"with property-key nested struct should be named Address: {actual}"
);
assert!(
!actual.contains("struct FooBar "),
"with property-key title FooBar should not be used: {actual}"
);
}
#[test]
fn cli_generate_rust_root_not_object_exits_nonzero() {
let schema_json = r#"{"type":"string"}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
!output.status.success(),
"root not object should exit non-zero"
);
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
stderr.contains("RootNotObject") || stderr.contains("root") || stderr.contains("object"),
"stderr should mention error: {stderr}"
);
}
#[test]
fn cli_generate_rust_deep_schema_succeeds() {
let mut inner: serde_json::Value = serde_json::json!({
"type": "object",
"properties": { "value": { "type": "string" } }
});
for _ in 0..20 {
inner = serde_json::json!({
"type": "object",
"properties": { "child": inner }
});
}
let schema_json = serde_json::to_string(&inner).expect("serialize schema");
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"deep schema should succeed: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn cli_validate_valid_payload_exit_zero() {
let schema_json = r#"{"type":"object","properties":{"x":{"type":"string"}}}"#;
let mut schema_file = tempfile::NamedTempFile::new().expect("temp schema");
std::io::Write::write_all(&mut schema_file, schema_json.as_bytes()).expect("write schema");
schema_file.flush().expect("flush schema");
let payload_json = r#"{"x":"ok"}"#;
let mut child = Command::new(jsonschemars_bin())
.args(["validate", "-s", schema_file.path().to_str().unwrap()])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn jsonschemars");
std::io::Write::write_all(child.stdin.as_mut().unwrap(), payload_json.as_bytes())
.expect("write payload");
let output = child.wait_with_output().expect("wait jsonschemars");
assert!(
output.status.success(),
"validate should succeed: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn cli_validate_invalid_payload_exit_nonzero_and_stderr() {
let schema_json = r#"{"type":"object","required":["id"]}"#;
let payload_json = r"{}";
let mut schema_file = tempfile::NamedTempFile::new().expect("temp schema");
std::io::Write::write_all(&mut schema_file, schema_json.as_bytes()).expect("write schema");
schema_file.flush().expect("flush schema");
let mut child = Command::new(jsonschemars_bin())
.args(["validate", "-s", schema_file.path().to_str().unwrap()])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
.expect("spawn jsonschemars");
std::io::Write::write_all(child.stdin.as_mut().unwrap(), payload_json.as_bytes())
.expect("write payload");
let output = child.wait_with_output().expect("wait jsonschemars");
assert!(!output.status.success(), "validate should fail");
let stderr = String::from_utf8(output.stderr).expect("utf8 stderr");
assert!(
stderr.contains("missing required"),
"stderr should mention missing required: {stderr}"
);
}
#[test]
fn integration_parse_and_generate() {
let json = r#"{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub id: String,
}
";
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_defs_ref() {
let json = r##"{
"$defs": {
"Address": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
},
"type": "object",
"properties": { "address": { "$ref": "#/$defs/Address" } },
"required": ["address"]
}"##;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Address {
pub city: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub address: Address,
}
";
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_optional_string() {
let json = r#"{"type":"object","properties":{"name":{"type":"string"}}}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub name: Option<String>,
}
";
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_enum_required() {
let json = r#"{"type":"object","properties":{"status":{"enum":["open","closed"]}},"required":["status"]}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub enum Status {
#[serde(rename = "closed")]
Closed,
#[serde(rename = "open")]
Open,
}
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub status: Status,
}
"#;
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_description_struct_and_field() {
let json = r#"{"type":"object","description":"Root type","properties":{"name":{"type":"string","description":"User name"}}}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
/// Root type
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
/// User name
pub name: Option<String>,
}
";
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_enum_optional() {
let json = r#"{"type":"object","properties":{"level":{"enum":["low","medium","high"]}}}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
assert!(actual.contains("pub enum Level"));
assert!(actual.contains("pub level: Option<Level>"));
assert!(actual.contains("Low"));
assert!(actual.contains("Medium"));
assert!(actual.contains("High"));
}
#[test]
fn integration_parse_and_generate_enum_non_rust_compliant() {
let json = r#"{"type":"object","properties":{"id":{"enum":["/8633","todd.griffin"]}},"required":["id"]}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub enum Id {
#[serde(rename = "/8633")]
E8633,
#[serde(rename = "todd.griffin")]
ToddGriffin,
}
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub id: Id,
}
"#;
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_enum_duplicate_values() {
let json = r#"{"type":"object","properties":{"t":{"enum":["A","A","A","a","a","a","a","a","a","a"]}},"required":["t"]}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub enum T {
#[serde(rename = "A")]
A0,
#[serde(rename = "a")]
A1,
}
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub t: T,
}
"#;
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_nested_object() {
let json = r#"{
"type": "object",
"properties": {
"first_name": { "type": "string" },
"address": {
"type": "object",
"properties": {
"street_address": { "type": "string" },
"city": { "type": "string" }
}
}
}
}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Address {
pub city: Option<String>,
pub street_address: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub address: Option<Address>,
pub first_name: Option<String>,
}
";
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_required_integer() {
let json =
r#"{"type":"object","properties":{"count":{"type":"integer"}},"required":["count"]}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub count: i64,
}
";
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_optional_integer() {
let json = r#"{"type":"object","properties":{"count":{"type":"integer"}}}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub count: Option<i64>,
}
";
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_required_float() {
let json = r#"{"type":"object","properties":{"value":{"type":"number"}},"required":["value"]}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub value: f64,
}
";
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_optional_float() {
let json = r#"{"type":"object","properties":{"value":{"type":"number"}}}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub value: Option<f64>,
}
";
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_required_boolean() {
let json =
r#"{"type":"object","properties":{"enabled":{"type":"boolean"}},"required":["enabled"]}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub enabled: bool,
}
";
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_optional_boolean() {
let json = r#"{"type":"object","properties":{"flag":{"type":"boolean"}}}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub flag: Option<bool>,
}
";
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_integer_with_minimum_maximum_emits_u8() {
let json = r#"{"type":"object","properties":{"byte":{"type":"integer","minimum":0,"maximum":255}},"required":["byte"]}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub byte: u8,
}
";
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_number_with_min_max_emits_f32() {
let json = r#"{"type":"object","properties":{"value":{"type":"number","minimum":0.5,"maximum":100.5}},"required":["value"]}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub value: f32,
}
";
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_array_required() {
let json = r#"{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"}}},"required":["tags"]}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub tags: Vec<String>,
}
";
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_array_unique_items_true() {
let json = r#"{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"},"uniqueItems":true}},"required":["tags"]}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
assert!(
actual.contains("pub tags: HashSet<String>"),
"expected HashSet<String> for uniqueItems true: {actual}"
);
assert!(
actual.contains("use std::collections::HashSet"),
"expected HashSet use: {actual}"
);
}
#[test]
fn integration_parse_and_generate_array_optional() {
let json =
r#"{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"}}}}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub tags: Option<Vec<String>>,
}
";
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_array_min_items_max_items() {
let json = r#"{"type":"object","properties":{"tags":{"type":"array","items":{"type":"string"},"minItems":2,"maxItems":5}},"required":["tags"]}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
#[json_schema(min_items = 2, max_items = 5)]
pub tags: Vec<String>,
}
";
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_string_min_length() {
let json = r#"{"type":"object","properties":{"name":{"type":"string","minLength":2}},"required":["name"]}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
#[json_schema(min_length = 2)]
pub name: String,
}
";
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_string_max_length() {
let json = r#"{"type":"object","properties":{"name":{"type":"string","maxLength":50}},"required":["name"]}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
#[json_schema(max_length = 50)]
pub name: String,
}
";
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_string_min_length_max_length() {
let json = r#"{"type":"object","properties":{"name":{"type":"string","minLength":2,"maxLength":50}},"required":["name"]}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
#[json_schema(min_length = 2, max_length = 50)]
pub name: String,
}
";
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_string_min_length_max_length_optional() {
let json =
r#"{"type":"object","properties":{"name":{"type":"string","minLength":2,"maxLength":50}}}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
#[json_schema(min_length = 2, max_length = 50)]
pub name: Option<String>,
}
";
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_string_pattern() {
let json = r#"{"type":"object","properties":{"name":{"type":"string","pattern":"^[a-z]+$"}},"required":["name"]}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = concat!(
"//! Generated by json-schema-rs. Do not edit manually.\n\n",
"use serde::{Deserialize, Serialize};\n\n",
"#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]\n",
"pub struct Root {\n",
" #[json_schema(pattern = \"^[a-z]+$\")]\n",
" pub name: String,\n",
"}\n\n"
);
assert_eq!(expected, actual);
}
#[test]
fn integration_validator_pattern_match_passes() {
let schema: JsonSchema =
JsonSchema::try_from(r#"{"type":"string","pattern":"^[0-9]+$"}"#).expect("parse");
let instance = serde_json::json!("123");
let expected: Result<(), _> = Ok(());
let actual: Result<(), _> = json_schema_rs::validate(&schema, &instance);
assert_eq!(expected, actual);
}
#[test]
fn integration_validator_pattern_mismatch_fails() {
let schema: JsonSchema =
JsonSchema::try_from(r#"{"type":"string","pattern":"^[0-9]+$"}"#).expect("parse");
let instance = serde_json::json!("12a3");
let actual: Result<(), _> = json_schema_rs::validate(&schema, &instance);
let errs = actual.unwrap_err();
let expected: Vec<json_schema_rs::ValidationError> =
vec![json_schema_rs::ValidationError::PatternMismatch {
instance_path: json_schema_rs::JsonPointer::root(),
pattern: "^[0-9]+$".to_string(),
value: "12a3".to_string(),
}];
assert_eq!(expected, errs);
}
#[test]
fn integration_parse_and_generate_array_of_objects() {
let json = r#"{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"}}}}},"required":["items"]}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Items {
pub name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
pub items: Vec<Items>,
}
";
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_hyphenated_property() {
let json = r#"{"type":"object","properties":{"foo-bar":{"type":"string"}}}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
let expected = r#"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
#[serde(rename = "foo-bar")]
pub foo_bar: Option<String>,
}
"#;
assert_eq!(expected, actual);
}
#[test]
fn integration_parse_and_generate_root_not_object_errors() {
let json = r#"{"type":"string"}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let actual = generate_rust(&[schema], &code_gen_settings);
assert!(actual.is_err(), "root not object should error");
}
/// Stable target directory for all "generated Rust build + deserialize" integration tests.
/// Path deps (json-schema-rs, json-schema-rs-macro) are built once and reused across scenarios.
fn integration_codegen_target_dir() -> PathBuf {
let manifest_dir =
std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR set by Cargo");
let target_dir: PathBuf = std::env::var("CARGO_TARGET_DIR").map_or_else(
|_| PathBuf::from(&manifest_dir).join("..").join("target"),
PathBuf::from,
);
target_dir.join("integration_codegen")
}
/// Cargo.toml content for a temp crate that compiles generated Rust (needs json-schema-rs and macro for `ToJsonSchema` derive).
#[expect(dead_code)]
fn temp_crate_cargo_toml_with_reverse_codegen() -> String {
temp_crate_cargo_toml_with_reverse_codegen_named("compile_test")
}
/// Cargo.toml content for a workspace member with the given package name.
fn temp_crate_cargo_toml_with_reverse_codegen_named(package_name: &str) -> String {
let manifest_dir =
std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR set by Cargo");
let workspace_root = std::path::Path::new(&manifest_dir)
.parent()
.expect("workspace root");
let json_schema_rs_path = workspace_root.join("json_schema_rs");
let macro_path = workspace_root.join("json_schema_rs_macro");
format!(
r#"[package]
name = "{}"
version = "0.0.1"
edition = "2024"
[lib]
path = "src/lib.rs"
[[bin]]
name = "{}"
path = "src/main.rs"
[dependencies]
serde = {{ version = "1", features = ["derive"] }}
serde_json = "1"
json-schema-rs = {{ path = "{}" }}
json-schema-rs-macro = {{ path = "{}" }}
"#,
package_name,
package_name,
json_schema_rs_path.display(),
macro_path.display()
)
}
/// Runs `cargo build` with `CARGO_TARGET_DIR` set to the shared `integration_codegen` path.
/// Asserts success; on failure panics with stderr.
#[expect(dead_code)]
fn assert_cargo_build(cwd: &Path, scenario_name: &str) {
let target_dir: PathBuf = integration_codegen_target_dir();
let build = Command::new("cargo")
.args(["build"])
.current_dir(cwd)
.env("CARGO_TARGET_DIR", &target_dir)
.output()
.expect("run cargo build");
assert!(
build.status.success(),
"scenario '{}': cargo build failed: stderr={}",
scenario_name,
String::from_utf8_lossy(&build.stderr)
);
}
/// Runs `cargo run --bin run` with `CARGO_TARGET_DIR` set. For a single crate, cwd is crate root.
#[expect(dead_code)]
fn assert_cargo_run_bin_run(cwd: &Path, scenario_name: &str) {
let target_dir: PathBuf = integration_codegen_target_dir();
let run = Command::new("cargo")
.args(["run", "--bin", "run"])
.current_dir(cwd)
.env("CARGO_TARGET_DIR", &target_dir)
.output()
.expect("run cargo run");
assert!(
run.status.success(),
"scenario '{}': cargo run failed: stderr={}",
scenario_name,
String::from_utf8_lossy(&run.stderr)
);
}
/// Runs `cargo build --workspace` with `CARGO_TARGET_DIR` set. Cwd must be workspace root.
fn assert_cargo_build_workspace(workspace_root: &Path) {
let target_dir: PathBuf = integration_codegen_target_dir();
let build = Command::new("cargo")
.args(["build", "--workspace"])
.current_dir(workspace_root)
.env("CARGO_TARGET_DIR", &target_dir)
.output()
.expect("run cargo build --workspace");
assert!(
build.status.success(),
"cargo build --workspace failed: stderr={}",
String::from_utf8_lossy(&build.stderr)
);
}
/// Runs `cargo run -p <package> --bin <package>` with `CARGO_TARGET_DIR` set. Cwd must be workspace root.
fn assert_cargo_run_package(workspace_root: &Path, package: &str, scenario_name: &str) {
let target_dir: PathBuf = integration_codegen_target_dir();
let run = Command::new("cargo")
.args(["run", "-p", package, "--bin", package])
.current_dir(workspace_root)
.env("CARGO_TARGET_DIR", &target_dir)
.output()
.expect("run cargo run");
assert!(
run.status.success(),
"scenario '{}': cargo run -p {} failed: stderr={}",
scenario_name,
package,
String::from_utf8_lossy(&run.stderr)
);
}
/// Builds map: directory path (relative) -> set of module names. Root is `PathBuf::new()`; subdirs e.g. "nested".
fn mod_rs_content_by_dir(output_relatives: &[PathBuf]) -> BTreeMap<PathBuf, BTreeSet<String>> {
let mut by_dir: BTreeMap<PathBuf, BTreeSet<String>> = BTreeMap::new();
for rel in output_relatives {
let path = Path::new(rel);
let components: Vec<_> = path.components().collect();
if components.is_empty() {
continue;
}
let module_name = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("schema")
.to_string();
if components.len() == 1 {
by_dir
.entry(PathBuf::new())
.or_default()
.insert(module_name);
} else {
let subdir_name = components[0].as_os_str().to_string_lossy().to_string();
by_dir
.entry(PathBuf::new())
.or_default()
.insert(subdir_name.clone());
by_dir
.entry(PathBuf::from(&subdir_name))
.or_default()
.insert(module_name);
}
}
by_dir
}
/// Writes one workspace member (Cargo.toml, src/) for the given scenario.
/// Uses `package_name` in Cargo.toml and in main.rs (replacing `compile_test`).
#[expect(clippy::too_many_lines)]
fn write_workspace_scenario_member(
scenarios_dir: &Path,
scenario_name: &str,
package_name: &str,
default_code_gen: &CodeGenSettings,
) {
let member_path = scenarios_dir.join(scenario_name);
let src = member_path.join("src");
fs::create_dir_all(&src).expect("create src");
fs::write(
member_path.join("Cargo.toml"),
temp_crate_cargo_toml_with_reverse_codegen_named(package_name),
)
.expect("write Cargo.toml");
#[expect(clippy::type_complexity)]
let (lib_rs, extra_files, main_rs_template): (Vec<u8>, Vec<(PathBuf, Vec<u8>)>, &str) =
match scenario_name {
"defs_ref" => {
let schema_json = r##"{
"$defs": {
"Address": {
"type": "object",
"properties": { "city": { "type": "string" } },
"required": ["city"]
}
},
"type": "object",
"properties": { "address": { "$ref": "#/$defs/Address" } },
"required": ["address"]
}"##;
let schema: JsonSchema = JsonSchema::try_from(schema_json).expect("parse schema");
let output = generate_rust(&[schema], default_code_gen).expect("generate");
let main_rs = r##"fn main() {
let v: compile_test::Root = serde_json::from_str(r#"{"address":{"city":"NYC"}}"#).unwrap();
assert_eq!(v.address.city.as_str(), "NYC");
}
"##;
(output.per_schema[0].clone(), vec![], main_rs)
}
"nested_modules" => {
let schema_root = r#"{"type":"object","properties":{"x":{"type":"string"}}}"#;
let schema_child = r#"{"type":"object","properties":{"y":{"type":"string"}}}"#;
let parsed_root: JsonSchema =
JsonSchema::try_from(schema_root).expect("parse root");
let parsed_child: JsonSchema =
JsonSchema::try_from(schema_child).expect("parse child");
let output = generate_rust(&[parsed_root, parsed_child], default_code_gen)
.expect("generate");
let output_relatives: Vec<PathBuf> =
vec![PathBuf::from("root.rs"), PathBuf::from("nested/child.rs")];
let by_dir = mod_rs_content_by_dir(&output_relatives);
let root_modules = by_dir.get(&PathBuf::new()).expect("root entry");
let lib_rs_content: String = root_modules
.iter()
.map(|m| format!("pub mod {m};"))
.collect::<Vec<_>>()
.join("\n")
+ "\n";
let nested_modules = by_dir.get(&PathBuf::from("nested")).expect("nested entry");
let nested_mod_content: String = nested_modules
.iter()
.map(|m| format!("pub mod {m};"))
.collect::<Vec<_>>()
.join("\n")
+ "\n";
let main_rs = r##"fn main() {
let _: compile_test::root::Root = serde_json::from_str(r#"{"x":"ok"}"#).unwrap();
let _: compile_test::nested::child::Root = serde_json::from_str(r#"{"y":"ok"}"#).unwrap();
}
"##;
let extra: Vec<(PathBuf, Vec<u8>)> = vec![
(PathBuf::from("root.rs"), output.per_schema[0].clone()),
(
PathBuf::from("nested/mod.rs"),
nested_mod_content.into_bytes(),
),
(
PathBuf::from("nested/child.rs"),
output.per_schema[1].clone(),
),
];
(lib_rs_content.into_bytes(), extra, main_rs)
}
"multi_schema" => {
let schema_a = r#"{"type":"object","properties":{"a":{"type":"string"}}}"#;
let schema_b = r#"{"type":"object","properties":{"b":{"type":"string"}}}"#;
let parsed_a: JsonSchema = JsonSchema::try_from(schema_a).expect("parse a");
let parsed_b: JsonSchema = JsonSchema::try_from(schema_b).expect("parse b");
let output =
generate_rust(&[parsed_a, parsed_b], default_code_gen).expect("generate");
let main_rs = r##"fn main() {
let _: compile_test::a::Root = serde_json::from_str(r#"{"a":"x"}"#).unwrap();
let _: compile_test::b::Root = serde_json::from_str(r#"{"b":"y"}"#).unwrap();
}
"##;
let lib_rs: Vec<u8> = "pub mod a;\npub mod b;\n".into();
let extra: Vec<(PathBuf, Vec<u8>)> = vec![
(PathBuf::from("a.rs"), output.per_schema[0].clone()),
(PathBuf::from("b.rs"), output.per_schema[1].clone()),
];
(lib_rs, extra, main_rs)
}
"hyphenated_paths" => {
let schema_a = r#"{"type":"object","properties":{"a":{"type":"string"}}}"#;
let schema_b = r#"{"type":"object","properties":{"b":{"type":"string"}}}"#;
let parsed_a: JsonSchema = JsonSchema::try_from(schema_a).expect("parse a");
let parsed_b: JsonSchema = JsonSchema::try_from(schema_b).expect("parse b");
let output =
generate_rust(&[parsed_a, parsed_b], default_code_gen).expect("generate");
let main_rs = r##"fn main() {
let _: compile_test::schema_1::Root = serde_json::from_str(r#"{"a":"x"}"#).unwrap();
let _: compile_test::sub_dir::schema_2::Root = serde_json::from_str(r#"{"b":"y"}"#).unwrap();
}
"##;
let lib_rs: Vec<u8> = "pub mod schema_1;\npub mod sub_dir;\n".into();
let extra: Vec<(PathBuf, Vec<u8>)> = vec![
(PathBuf::from("schema_1.rs"), output.per_schema[0].clone()),
(
PathBuf::from("sub_dir/mod.rs"),
"pub mod schema_2;\n".into(),
),
(
PathBuf::from("sub_dir/schema_2.rs"),
output.per_schema[1].clone(),
),
];
(lib_rs, extra, main_rs)
}
"dedupe_two_identical" => {
let schema_json = r#"{"type":"object","properties":{"id":{"type":"string"}}}"#;
let parsed_a: JsonSchema = JsonSchema::try_from(schema_json).expect("parse a");
let parsed_b: JsonSchema = JsonSchema::try_from(schema_json).expect("parse b");
let code_gen: CodeGenSettings = CodeGenSettings::builder()
.dedupe_mode(DedupeMode::Full)
.build();
let output = generate_rust(&[parsed_a, parsed_b], &code_gen).expect("generate");
let shared_bytes: Vec<u8> = output
.shared
.clone()
.expect("dedupe with two identical schemas produces shared");
let a_rs_content: String = String::from_utf8(output.per_schema[0].clone())
.expect("utf8")
.replace("pub use crate::", "pub use super::shared::");
let b_rs_content: String = String::from_utf8(output.per_schema[1].clone())
.expect("utf8")
.replace("pub use crate::", "pub use super::shared::");
let main_rs = r##"fn main() {
let _: compile_test::a::Root = serde_json::from_str(r#"{"id":"x"}"#).unwrap();
let _: compile_test::b::Root = serde_json::from_str(r#"{"id":"y"}"#).unwrap();
}
"##;
let lib_rs: Vec<u8> = "pub mod shared;\npub mod a;\npub mod b;\n".into();
let extra: Vec<(PathBuf, Vec<u8>)> = vec![
(PathBuf::from("shared.rs"), shared_bytes),
(PathBuf::from("a.rs"), a_rs_content.into_bytes()),
(PathBuf::from("b.rs"), b_rs_content.into_bytes()),
];
(lib_rs, extra, main_rs)
}
"dedupe_nested_output" => {
let schema_json = r#"{"type":"object","properties":{"id":{"type":"string"}}}"#;
let parsed_a: JsonSchema = JsonSchema::try_from(schema_json).expect("parse a");
let parsed_b: JsonSchema = JsonSchema::try_from(schema_json).expect("parse b");
let code_gen: CodeGenSettings = CodeGenSettings::builder()
.dedupe_mode(DedupeMode::Full)
.build();
let output = generate_rust(&[parsed_a, parsed_b], &code_gen).expect("generate");
let shared_bytes: Vec<u8> = output
.shared
.clone()
.expect("dedupe with two identical schemas produces shared");
let a_rs_content: String = String::from_utf8(output.per_schema[0].clone())
.expect("utf8")
.replace("pub use crate::", "pub use super::shared::");
let b_rs_content: String = String::from_utf8(output.per_schema[1].clone())
.expect("utf8")
.replace("pub use crate::", "pub use super::shared::");
let main_rs = r##"fn main() {
let _: compile_test::generated::a::Root = serde_json::from_str(r#"{"id":"x"}"#).unwrap();
let _: compile_test::generated::b::Root = serde_json::from_str(r#"{"id":"y"}"#).unwrap();
}
"##;
let lib_rs: Vec<u8> = "pub mod generated;\n".into();
let generated_mod: Vec<u8> = "pub mod shared;\npub mod a;\npub mod b;\n".into();
let extra: Vec<(PathBuf, Vec<u8>)> = vec![
(PathBuf::from("generated/mod.rs"), generated_mod),
(PathBuf::from("generated/shared.rs"), shared_bytes),
(PathBuf::from("generated/a.rs"), a_rs_content.into_bytes()),
(PathBuf::from("generated/b.rs"), b_rs_content.into_bytes()),
];
(lib_rs, extra, main_rs)
}
"model_name_source_property_key" => {
let schema_json = r#"{"type":"object","properties":{"address":{"type":"object","title":"FooBar","properties":{"city":{"type":"string"}}}}}"#;
let schema: JsonSchema = JsonSchema::try_from(schema_json).expect("parse schema");
let code_gen: CodeGenSettings = CodeGenSettings::builder()
.model_name_source(ModelNameSource::PropertyKeyFirst)
.build();
let output = generate_rust(&[schema], &code_gen).expect("generate");
let main_rs = r##"fn main() {
let v: compile_test::Root = serde_json::from_str(r#"{"address":{"city":"NYC"}}"#).unwrap();
assert!(v.address.is_some());
assert_eq!(v.address.as_ref().unwrap().city.as_deref(), Some("NYC"));
}
"##;
(output.per_schema[0].clone(), vec![], main_rs)
}
"deep_nesting" => {
const DEPTH: usize = 15;
let mut inner: JsonSchema = JsonSchema {
type_: Some("object".to_string()),
properties: {
let mut m = std::collections::BTreeMap::new();
m.insert(
"value".to_string(),
JsonSchema {
type_: Some("string".to_string()),
..Default::default()
},
);
m
},
title: Some("Leaf".to_string()),
..Default::default()
};
for i in (0..DEPTH).rev() {
let mut wrap: JsonSchema = JsonSchema {
type_: Some("object".to_string()),
properties: std::collections::BTreeMap::new(),
title: Some(format!("Level{i}")),
..Default::default()
};
wrap.properties.insert("child".to_string(), inner);
inner = wrap;
}
let output = generate_rust(&[inner], default_code_gen).expect("generate");
let main_rs = r##"fn main() {
let _: compile_test::Level0 = serde_json::from_str(r#"{"child":{"child":{"child":{}}}}"#).unwrap();
}
"##;
(output.per_schema[0].clone(), vec![], main_rs)
}
"allof_merged" => {
let schema_json = r#"{"allOf":[{"type":"object","properties":{"a":{"type":"string"}}},{"type":"object","properties":{"b":{"type":"integer"}},"required":["b"]}]}"#;
let schema: JsonSchema =
JsonSchema::try_from(schema_json).expect("parse allOf schema");
let output = generate_rust(&[schema], default_code_gen).expect("generate");
let main_rs = r##"fn main() {
let _: compile_test::Root = serde_json::from_str(r#"{"a":"x","b":1}"#).unwrap();
}
"##;
(output.per_schema[0].clone(), vec![], main_rs)
}
"oneof_union" => {
let schema_json = r#"{"oneOf":[{"type":"object","properties":{"a":{"type":"string"}}},{"type":"object","properties":{"b":{"type":"integer"}}}]}"#;
let schema: JsonSchema =
JsonSchema::try_from(schema_json).expect("parse oneOf schema");
let output = generate_rust(&[schema], default_code_gen).expect("generate");
let main_rs = r##"fn main() {
let v0: compile_test::RootOneOf = serde_json::from_str(r#"{"Variant0":{"a":"x"}}"#).unwrap();
let v1: compile_test::RootOneOf = serde_json::from_str(r#"{"Variant1":{"b":42}}"#).unwrap();
assert!(matches!(v0, compile_test::RootOneOf::Variant0(_)));
assert!(matches!(v1, compile_test::RootOneOf::Variant1(_)));
}
"##;
(output.per_schema[0].clone(), vec![], main_rs)
}
"additional_properties_false" => {
let schema_json = r#"{"type":"object","properties":{"id":{"type":"string"}},"required":["id"],"additionalProperties":false}"#;
let schema: JsonSchema = JsonSchema::try_from(schema_json).expect("parse schema");
let output = generate_rust(&[schema], default_code_gen).expect("generate");
let main_rs = r##"fn main() {
let root: compile_test::Root = serde_json::from_str(r#"{"id":"x"}"#).unwrap();
assert_eq!(root.id, "x");
let err = serde_json::from_str::<compile_test::Root>(r#"{"id":"x","extra":1}"#).unwrap_err();
assert!(err.to_string().contains("unknown field"));
}
"##;
(output.per_schema[0].clone(), vec![], main_rs)
}
"additional_properties_schema" => {
let schema_json = r#"{"type":"object","properties":{"id":{"type":"string"}},"required":["id"],"additionalProperties":{"type":"string"}}"#;
let schema: JsonSchema = JsonSchema::try_from(schema_json).expect("parse schema");
let output = generate_rust(&[schema], default_code_gen).expect("generate");
let main_rs = r##"fn main() {
let root: compile_test::Root = serde_json::from_str(r#"{"id":"x"}"#).unwrap();
assert_eq!(root.id, "x");
assert!(root.additional.is_empty());
let with_extra = compile_test::Root {
id: "y".to_string(),
additional: [("foo".to_string(), "bar".to_string()), ("baz".to_string(), "quux".to_string())].into_iter().collect(),
};
let json = serde_json::to_string(&with_extra).unwrap();
let back: compile_test::Root = serde_json::from_str(&json).unwrap();
assert_eq!(back.id, "y");
assert_eq!(back.additional.get("foo").map(String::as_str), Some("bar"));
assert_eq!(back.additional.get("baz").map(String::as_str), Some("quux"));
}
"##;
(output.per_schema[0].clone(), vec![], main_rs)
}
"string_pattern" => {
let schema_json = r#"{"type":"object","properties":{"name":{"type":"string","pattern":"^[a-z]+$"}},"required":["name"]}"#;
let schema: JsonSchema = JsonSchema::try_from(schema_json).expect("parse schema");
let output = generate_rust(&[schema], default_code_gen).expect("generate");
let main_rs = r##"fn main() {
let root: compile_test::Root = serde_json::from_str(r#"{"name":"abc"}"#).unwrap();
assert_eq!(root.name, "abc");
}
"##;
(output.per_schema[0].clone(), vec![], main_rs)
}
"default" => {
// Optional property with default: missing key gets schema default at deserialize time.
// Single property with default parses reliably (multiple properties with default can hit a parse quirk).
let schema_json =
r#"{"type":"object","properties":{"name":{"type":"string","default":"foo"}}}"#;
let schema: JsonSchema = JsonSchema::try_from(schema_json).expect("parse schema");
let output = generate_rust(&[schema], default_code_gen).expect("generate");
let main_rs = r##"fn main() {
let root: compile_test::Root = serde_json::from_str(r#"{}"#).unwrap();
assert_eq!(root.name.as_deref(), Some("foo"));
let partial: compile_test::Root = serde_json::from_str(r#"{"name":"bar"}"#).unwrap();
assert_eq!(partial.name.as_deref(), Some("bar"));
}
"##;
(output.per_schema[0].clone(), vec![], main_rs)
}
"kitchen_sink" => {
// One schema exercising many features; main asserts each (same as the individual scenario tests).
let schema_json = r#"{"type":"object","description":"Root","properties":{"id":{"type":"string","description":"Identifier"},"enabled":{"type":"boolean"},"flag":{"type":"boolean"},"value_required":{"type":"number"},"value_optional":{"type":"number"},"count_required":{"type":"integer"},"count_optional":{"type":"integer"},"byte":{"type":"integer","minimum":0,"maximum":255},"value_f32":{"type":"number","minimum":0.0,"maximum":100.0},"tags_required":{"type":"array","items":{"type":"string"}},"tags_optional":{"type":"array","items":{"type":"string"}},"tags_unique":{"type":"array","items":{"type":"string"},"uniqueItems":true},"tags_min_max":{"type":"array","items":{"type":"string"},"minItems":2,"maxItems":5},"address":{"type":"object","properties":{"city":{"type":"string"},"street_address":{"type":"string"}}},"name_bounded":{"type":"string","minLength":2,"maxLength":50},"foo-bar":{"type":"string"},"status":{"enum":["open","closed"]},"fixed":{"const":"only"},"name_opt":{"type":"string"}},"required":["id","enabled","value_required","count_required","byte","value_f32","tags_required","tags_unique","tags_min_max","name_bounded","status","fixed"]}"#;
let schema: JsonSchema = JsonSchema::try_from(schema_json).expect("parse schema");
let output = generate_rust(&[schema], default_code_gen).expect("generate");
let main_rs = r##"fn main() {
use std::collections::HashSet;
// single_schema / description: required id
let root: compile_test::Root = serde_json::from_str(r#"{"id":"x","enabled":true,"value_required":3.14,"count_required":42,"byte":42,"value_f32":3.14,"tags_required":["a","b","c"],"tags_unique":["a","b","c"],"tags_min_max":["a","b","c"],"name_bounded":"Alice","status":"open","fixed":"only"}"#).unwrap();
assert_eq!(root.id, "x".to_string());
// required_boolean
assert_eq!(root.enabled, true);
// required_float
assert_eq!(root.value_required, 3.14);
// required_integer
assert_eq!(root.count_required, 42);
// integer_with_min_max_u8
assert_eq!(root.byte, 42);
// number_with_min_max_f32
assert_eq!(root.value_f32, 3.14);
// array_required
assert_eq!(root.tags_required, vec!["a".to_string(), "b".to_string(), "c".to_string()]);
// unique_items
let expected_set: HashSet<String> = ["a","b","c"].into_iter().map(String::from).collect();
assert_eq!(root.tags_unique, expected_set);
// min_items_max_items
assert_eq!(root.tags_min_max, vec!["a".to_string(), "b".to_string(), "c".to_string()]);
// string_min_length_max_length
assert_eq!(root.name_bounded, "Alice".to_string());
// enum_basic
assert_eq!(root.status, compile_test::Status::Open);
// const_string_property
assert_eq!(root.fixed, compile_test::Fixed::Only);
// optional_float
let r2: compile_test::Root = serde_json::from_str(r#"{"id":"y","enabled":false,"value_required":1.0,"value_optional":2.5,"count_required":0,"byte":0,"value_f32":0.0,"tags_required":[],"tags_unique":[],"tags_min_max":["x","y"],"name_bounded":"ab","status":"closed","fixed":"only"}"#).unwrap();
assert_eq!(r2.value_optional, Some(2.5));
// optional_integer
let r3: compile_test::Root = serde_json::from_str(r#"{"id":"z","enabled":true,"value_required":0.0,"count_required":0,"count_optional":1,"byte":0,"value_f32":0.0,"tags_required":[],"tags_unique":[],"tags_min_max":["a","b"],"name_bounded":"xy","status":"open","fixed":"only"}"#).unwrap();
assert_eq!(r3.count_optional, Some(1));
// array_optional
let r4: compile_test::Root = serde_json::from_str(r#"{"id":"w","enabled":true,"value_required":0.0,"count_required":0,"byte":0,"value_f32":0.0,"tags_required":[],"tags_optional":["x","y"],"tags_unique":[],"tags_min_max":["a","b"],"name_bounded":"ab","status":"open","fixed":"only"}"#).unwrap();
assert_eq!(r4.tags_optional, Some(vec!["x".to_string(), "y".to_string()]));
// optional_string
let r_opt: compile_test::Root = serde_json::from_str(r#"{"id":"o","enabled":false,"value_required":0.0,"count_required":0,"byte":0,"value_f32":0.0,"tags_required":[],"tags_unique":[],"tags_min_max":["a","b"],"name_bounded":"ab","status":"open","fixed":"only","name_opt":"optional"}"#).unwrap();
assert_eq!(r_opt.name_opt.as_deref(), Some("optional"));
// optional_boolean
let r_flag: compile_test::Root = serde_json::from_str(r#"{"id":"f","enabled":true,"value_required":0.0,"count_required":0,"byte":0,"value_f32":0.0,"tags_required":[],"tags_unique":[],"tags_min_max":["a","b"],"name_bounded":"ab","status":"open","fixed":"only","flag":false}"#).unwrap();
assert_eq!(r_flag.flag, Some(false));
// nested_object
let r5: compile_test::Root = serde_json::from_str(r#"{"id":"n","enabled":true,"value_required":0.0,"count_required":0,"byte":0,"value_f32":0.0,"tags_required":[],"tags_unique":[],"tags_min_max":["a","b"],"name_bounded":"ab","status":"open","fixed":"only","address":{"city":"NYC","street_address":"5th Ave"}}"#).unwrap();
assert!(r5.address.is_some());
assert_eq!(r5.address.as_ref().unwrap().city.as_deref(), Some("NYC"));
assert_eq!(r5.address.as_ref().unwrap().street_address.as_deref(), Some("5th Ave"));
// hyphenated_property
let r6: compile_test::Root = serde_json::from_str(r#"{"id":"h","enabled":false,"value_required":0.0,"count_required":0,"byte":0,"value_f32":0.0,"tags_required":[],"tags_unique":[],"tags_min_max":["a","b"],"name_bounded":"ab","status":"open","fixed":"only","foo-bar":"baz"}"#).unwrap();
assert_eq!(r6.foo_bar.as_deref(), Some("baz"));
}
"##;
(output.per_schema[0].clone(), vec![], main_rs)
}
"boolean_basic" => {
let schema_json = r#"{"type":"object","properties":{"enabled":{"type":"boolean"},"flag":{"type":"boolean"}},"required":["enabled"]}"#;
let schema: JsonSchema = JsonSchema::try_from(schema_json).expect("parse schema");
let output = generate_rust(&[schema], default_code_gen).expect("generate");
let main_rs = r##"fn main() {
let root: compile_test::Root = serde_json::from_str(r#"{"enabled":true,"flag":false}"#).unwrap();
assert_eq!(root.enabled, true);
assert_eq!(root.flag, Some(false));
let minimal: compile_test::Root = serde_json::from_str(r#"{"enabled":false}"#).unwrap();
assert_eq!(minimal.enabled, false);
assert_eq!(minimal.flag, None);
}
"##;
(output.per_schema[0].clone(), vec![], main_rs)
}
"round_trip" => {
// All six round-trip checks in one binary (same assertions as the six round_trip_* scenarios).
let schema_basic = r#"{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id"],"title":"Root"}"#;
let schema_integer = r#"{"type":"object","properties":{"id":{"type":"string"},"count":{"type":"integer"}},"required":["id"],"title":"Root"}"#;
let schema_number = r#"{"type":"object","properties":{"id":{"type":"string"},"count":{"type":"integer"},"value":{"type":"number"}},"required":["id"],"title":"Root"}"#;
let schema_int_min_max = r#"{"type":"object","properties":{"byte":{"type":"integer","minimum":0,"maximum":255}},"required":["byte"],"title":"Root"}"#;
let schema_number_min_max = r#"{"type":"object","properties":{"value":{"type":"number","minimum":0.5,"maximum":100.5}},"required":["value"],"title":"Root"}"#;
let schema_string_min_max = r#"{"type":"object","properties":{"name":{"type":"string","minLength":2,"maxLength":50}},"required":["name"]}"#;
let schemas: Vec<JsonSchema> = [
schema_basic,
schema_integer,
schema_number,
schema_int_min_max,
schema_number_min_max,
schema_string_min_max,
]
.iter()
.map(|s| JsonSchema::try_from(*s).expect("parse"))
.collect();
let output = generate_rust(&schemas, default_code_gen).expect("generate");
let lib_rs: Vec<u8> = "pub mod basic;\npub mod integer;\npub mod number;\npub mod integer_min_max;\npub mod number_min_max;\npub mod string_min_max;\n".into();
let extra: Vec<(PathBuf, Vec<u8>)> = vec![
(PathBuf::from("basic.rs"), output.per_schema[0].clone()),
(PathBuf::from("integer.rs"), output.per_schema[1].clone()),
(PathBuf::from("number.rs"), output.per_schema[2].clone()),
(
PathBuf::from("integer_min_max.rs"),
output.per_schema[3].clone(),
),
(
PathBuf::from("number_min_max.rs"),
output.per_schema[4].clone(),
),
(
PathBuf::from("string_min_max.rs"),
output.per_schema[5].clone(),
),
];
let main_rs = r#"
fn main() {
use json_schema_rs::{JsonSchema, ToJsonSchema};
// round_trip_basic
let schema = compile_test::basic::Root::json_schema();
let json: String = (&schema).try_into().expect("serialize");
let reparsed: JsonSchema = JsonSchema::try_from(json.as_str()).expect("parse");
assert_eq!(schema, reparsed, "round-trip basic");
// round_trip_integer
let schema = compile_test::integer::Root::json_schema();
let json: String = (&schema).try_into().expect("serialize");
let reparsed: JsonSchema = JsonSchema::try_from(json.as_str()).expect("parse");
assert_eq!(schema, reparsed, "round-trip integer");
// round_trip_number
let schema = compile_test::number::Root::json_schema();
let json: String = (&schema).try_into().expect("serialize");
let reparsed: JsonSchema = JsonSchema::try_from(json.as_str()).expect("parse");
assert_eq!(schema, reparsed, "round-trip number");
// round_trip_integer_min_max
let schema = compile_test::integer_min_max::Root::json_schema();
let json: String = (&schema).try_into().expect("serialize");
let reparsed: JsonSchema = JsonSchema::try_from(json.as_str()).expect("parse");
assert_eq!(schema, reparsed, "round-trip integer min max");
// round_trip_number_min_max (tolerance for f32)
let schema = compile_test::number_min_max::Root::json_schema();
let json: String = (&schema).try_into().expect("serialize");
let reparsed: JsonSchema = JsonSchema::try_from(json.as_str()).expect("parse");
let value_schema = schema.properties.get("value").expect("value property");
let value_reparsed = reparsed.properties.get("value").expect("value property");
assert_eq!(value_schema.type_, value_reparsed.type_, "type must match");
let (min_s, max_s) = (value_schema.minimum.unwrap(), value_schema.maximum.unwrap());
let (min_r, max_r) = (value_reparsed.minimum.unwrap(), value_reparsed.maximum.unwrap());
let tol = 1e32_f64;
assert!((min_s - min_r).abs() < tol && (max_s - max_r).abs() < tol, "min/max must round-trip within tolerance");
// round_trip_string_min_length_max_length
let schema = compile_test::string_min_max::Root::json_schema();
let json: String = (&schema).try_into().expect("serialize");
let reparsed: JsonSchema = JsonSchema::try_from(json.as_str()).expect("parse");
assert_eq!(schema, reparsed, "round-trip string min max");
}
"#;
(lib_rs, extra, main_rs)
}
"enum_non_rust_compliant" => {
let schema_json = r#"{"type":"object","properties":{"id":{"enum":["/8633","todd.griffin"]}},"required":["id"]}"#;
let schema: JsonSchema = JsonSchema::try_from(schema_json).expect("parse schema");
let output = generate_rust(&[schema], default_code_gen).expect("generate");
let main_rs = r##"fn main() {
let root: compile_test::Root = serde_json::from_str(r#"{"id":"/8633"}"#).unwrap();
assert_eq!(root.id, compile_test::Id::E8633);
let root2: compile_test::Root = serde_json::from_str(r#"{"id":"todd.griffin"}"#).unwrap();
assert_eq!(root2.id, compile_test::Id::ToddGriffin);
}
"##;
(output.per_schema[0].clone(), vec![], main_rs)
}
"enum_variants" => {
// Three enum scenarios (collision, dedupe, duplicate_values) in one member; basic enum is in kitchen_sink.
// Use DedupeMode::Disabled so each per_schema is self-contained (no shared buffer); otherwise enums are deduped and per_schema has "pub use crate::T" etc.
let code_gen: CodeGenSettings = CodeGenSettings::builder()
.dedupe_mode(DedupeMode::Disabled)
.build();
let schema_collision =
r#"{"type":"object","properties":{"t":{"enum":["a","A"]}},"required":["t"]}"#;
let schema_dedupe = r#"{"type":"object","properties":{"a":{"enum":["x","y"]},"b":{"enum":["x","y"]}},"required":["a"]}"#;
let schema_duplicate = r#"{"type":"object","properties":{"t":{"enum":["A","A","A","a","a","a","a","a","a","a"]}},"required":["t"]}"#;
let schemas: Vec<JsonSchema> = [schema_collision, schema_dedupe, schema_duplicate]
.iter()
.map(|s| JsonSchema::try_from(*s).expect("parse"))
.collect();
let output = generate_rust(&schemas, &code_gen).expect("generate");
let lib_rs: Vec<u8> =
"pub mod collision;\npub mod dedupe;\npub mod duplicate_values;\n".into();
let extra: Vec<(PathBuf, Vec<u8>)> = vec![
(PathBuf::from("collision.rs"), output.per_schema[0].clone()),
(PathBuf::from("dedupe.rs"), output.per_schema[1].clone()),
(
PathBuf::from("duplicate_values.rs"),
output.per_schema[2].clone(),
),
];
let main_rs = r##"fn main() {
// enum_collision
let root: compile_test::collision::Root = serde_json::from_str(r#"{"t":"A"}"#).unwrap();
assert_eq!(root.t, compile_test::collision::T::A0);
let root2: compile_test::collision::Root = serde_json::from_str(r#"{"t":"a"}"#).unwrap();
assert_eq!(root2.t, compile_test::collision::T::A1);
// enum_dedupe
let root: compile_test::dedupe::Root = serde_json::from_str(r#"{"a":"x","b":"y"}"#).unwrap();
assert_eq!(root.a, compile_test::dedupe::A::X);
assert_eq!(root.b, Some(compile_test::dedupe::A::Y));
// enum_duplicate_values
let root: compile_test::duplicate_values::Root = serde_json::from_str(r#"{"t":"A"}"#).unwrap();
assert_eq!(root.t, compile_test::duplicate_values::T::A0);
let root2: compile_test::duplicate_values::Root = serde_json::from_str(r#"{"t":"a"}"#).unwrap();
assert_eq!(root2.t, compile_test::duplicate_values::T::A1);
}
"##;
(lib_rs, extra, main_rs)
}
"examples_annotation" => {
let schema_json = r#"{"type":"object","properties":{"x":{"type":"string"}},"required":["x"],"examples":["foo"]}"#;
let schema: JsonSchema = JsonSchema::try_from(schema_json).expect("parse schema");
let output = generate_rust(&[schema], default_code_gen).expect("generate");
let main_rs = r##"fn main() {
let _: compile_test::Root = serde_json::from_str(r#"{"x":"foo"}"#).unwrap();
}
"##;
(output.per_schema[0].clone(), vec![], main_rs)
}
"deprecated_property" => {
// Property with deprecated: true; valid instance still deserializes.
let schema_json = r#"{"type":"object","properties":{"legacy":{"type":"string","deprecated":true}}}"#;
let schema: JsonSchema = JsonSchema::try_from(schema_json).expect("parse schema");
let output = generate_rust(&[schema], default_code_gen).expect("generate");
let main_rs = r##"fn main() {
let root: compile_test::Root = serde_json::from_str(r#"{"legacy":"still-valid"}"#).unwrap();
assert_eq!(root.legacy.as_deref(), Some("still-valid"));
}
"##;
(output.per_schema[0].clone(), vec![], main_rs)
}
_ => panic!("unknown scenario: {scenario_name}"),
};
fs::write(src.join("lib.rs"), &lib_rs).expect("write lib.rs");
for (rel, content) in extra_files {
let p = src.join(&rel);
if let Some(parent) = p.parent() {
fs::create_dir_all(parent).expect("create parent");
}
fs::write(&p, content).expect("write file");
}
let main_rs: String = main_rs_template.replace("compile_test", package_name);
fs::write(src.join("main.rs"), main_rs).expect("write main.rs");
}
#[cfg(feature = "uuid")]
#[test]
fn integration_parse_and_generate_uuid_property() {
let json = r#"{"type":"object","properties":{"id":{"type":"string","format":"uuid"}},"required":["id"]}"#;
let schema: JsonSchema = JsonSchema::try_from(json).expect("parse schema");
let code_gen_settings: CodeGenSettings = CodeGenSettings::builder().build();
let output = generate_rust(&[schema], &code_gen_settings).expect("generate");
let actual = String::from_utf8(output.per_schema[0].clone()).expect("utf8");
assert!(
actual.contains("pub id: Uuid,"),
"required uuid field should be Uuid: {actual}"
);
assert!(
actual.contains("use uuid::Uuid;"),
"uuid use statement should be present: {actual}"
);
}
#[cfg(feature = "uuid")]
#[test]
fn cli_generate_rust_uuid_property() {
let schema_json = r#"{"type":"object","properties":{"id":{"type":"string","format":"uuid"}},"required":["id"]}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
assert!(
actual.contains("pub id: Uuid,"),
"required uuid field should be Uuid: {actual}"
);
assert!(
actual.contains("use uuid::Uuid;"),
"uuid use statement should be present: {actual}"
);
}
#[test]
fn cli_generate_rust_additional_properties_false() {
let schema_json = r#"{"type":"object","properties":{"id":{"type":"string"}},"required":["id"],"additionalProperties":false}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
assert!(
actual.contains("#[serde(deny_unknown_fields)]"),
"additionalProperties: false should emit deny_unknown_fields: {actual}"
);
}
#[test]
fn cli_generate_rust_additional_properties_schema() {
let schema_json = r#"{"type":"object","properties":{"id":{"type":"string"}},"required":["id"],"additionalProperties":{"type":"string"}}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let out_path = out_dir.path().join("schema.rs");
let actual = std::fs::read_to_string(&out_path).expect("read output");
assert!(
actual.contains("BTreeMap<String, String>"),
"additionalProperties schema string should emit BTreeMap<String, String>: {actual}"
);
assert!(
actual.contains("pub additional:"),
"additionalProperties schema should emit 'additional' field: {actual}"
);
}
#[test]
fn cli_generate_rust_deprecated_property_emits_deprecated_attr() {
let schema_json =
r#"{"type":"object","properties":{"legacy":{"type":"string","deprecated":true}}}"#;
let temp_dir = tempfile::tempdir().expect("temp dir");
let schema_path = temp_dir.path().join("schema.json");
std::fs::write(&schema_path, schema_json).expect("write schema");
let out_dir = tempfile::tempdir().expect("temp out dir");
let output = Command::new(jsonschemars_bin())
.args([
"generate",
"rust",
"-o",
out_dir.path().to_str().unwrap(),
schema_path.to_str().unwrap(),
])
.output()
.expect("run jsonschemars");
assert!(
output.status.success(),
"exit success: stderr={}",
String::from_utf8_lossy(&output.stderr)
);
let actual_path = out_dir.path().join("schema.rs");
let actual: String = std::fs::read_to_string(&actual_path).expect("read output");
let expected = r"//! Generated by json-schema-rs. Do not edit manually.
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, json_schema_rs_macro::ToJsonSchema)]
pub struct Root {
#[deprecated]
pub legacy: Option<String>,
}
";
assert_eq!(expected, actual);
}
#[test]
fn generated_rust_build_and_deserialize_all_scenarios() {
let default_code_gen: CodeGenSettings = CodeGenSettings::builder().build();
let workspace_root = tempfile::tempdir().expect("workspace root");
let scenarios_dir = workspace_root.path().join("scenarios");
fs::create_dir_all(&scenarios_dir).expect("create scenarios dir");
let scenario_list: Vec<&str> = vec![
"kitchen_sink",
"boolean_basic",
"round_trip",
"enum_non_rust_compliant",
"enum_variants",
"nested_modules",
"multi_schema",
"hyphenated_paths",
"dedupe_two_identical",
"dedupe_nested_output",
"model_name_source_property_key",
"defs_ref",
"deep_nesting",
"allof_merged",
"oneof_union",
"additional_properties_false",
"additional_properties_schema",
"string_pattern",
"default",
"examples_annotation",
"deprecated_property",
];
for name in &scenario_list {
let package_name: String = format!("compile_test_{name}");
write_workspace_scenario_member(&scenarios_dir, name, &package_name, &default_code_gen);
}
let members: String = scenario_list
.iter()
.map(|n| format!("\"scenarios/{n}\""))
.collect::<Vec<_>>()
.join(", ");
let workspace_toml = format!(
r"[workspace]
members = [{members}]
"
);
fs::write(workspace_root.path().join("Cargo.toml"), workspace_toml)
.expect("write workspace Cargo.toml");
assert_cargo_build_workspace(workspace_root.path());
for name in &scenario_list {
let package_name: String = format!("compile_test_{name}");
assert_cargo_run_package(workspace_root.path(), &package_name, name);
}
}