use crate::genson_rs::{build_json_schema, build_json_schema_into, get_builder, BuildConfig};
use crate::{debug, profile, profile_verbose};
use rayon::prelude::*;
use serde::de::Error as DeError;
use serde::Deserialize;
use serde_json::{json, Value};
use std::borrow::Cow;
use std::collections::HashSet;
use std::panic::{self, AssertUnwindSafe};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::genson_rs::SchemaBuilder;
pub(crate) mod core;
pub use core::*;
mod map_inference;
use map_inference::*;
const MAX_JSON_ERROR_LENGTH: usize = 100;
const PARALLEL_THRESHOLD: usize = 10;
pub(crate) fn get_rss_bytes() -> Option<usize> {
let status = std::fs::read_to_string("/proc/self/status").ok()?;
for line in status.lines() {
if line.starts_with("VmRSS:") {
let parts: Vec<&str> = line.split_whitespace().collect();
if parts.len() >= 2 {
let kb: usize = parts[1].parse().ok()?;
return Some(kb * 1024); }
}
}
None
}
pub(crate) fn format_bytes(bytes: usize) -> String {
const MB: usize = 1024 * 1024;
const GB: usize = 1024 * 1024 * 1024;
if bytes >= GB {
format!("{:.2} GB", bytes as f64 / GB as f64)
} else if bytes >= MB {
format!("{:.2} MB", bytes as f64 / MB as f64)
} else {
format!("{:.2} KB", bytes as f64 / 1024.0)
}
}
fn current_time_hms() -> String {
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
let total_seconds = now.as_secs() % 86_400; let hours = total_seconds / 3600;
let minutes = (total_seconds % 3600) / 60;
let seconds = (total_seconds % 60) as f64 + now.subsec_millis() as f64 / 1000.0;
format!("{:02}:{:02}:{:04.1}", hours, minutes, seconds)
}
fn validate_json(s: &str) -> Result<(), serde_json::Error> {
let mut de = serde_json::Deserializer::from_str(s);
serde::de::IgnoredAny::deserialize(&mut de)?; de.end()
}
fn validate_ndjson(s: &str) -> Result<(), serde_json::Error> {
for line in s.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
validate_json(trimmed)?; }
Ok(())
}
pub fn reorder_unions(schema: &mut Value) {
match schema {
Value::Object(obj) => {
if let Some(Value::Array(types)) = obj.get_mut("type") {
if !(types.len() == 2 && types.iter().any(|t| t == "null")) {
types.sort_by_key(type_rank);
}
}
for v in obj.values_mut() {
reorder_unions(v);
}
}
Value::Array(arr) => {
if arr.len() >= PARALLEL_THRESHOLD {
arr.par_iter_mut().for_each(reorder_unions);
} else {
for v in arr {
reorder_unions(v);
}
}
}
_ => {}
}
}
pub fn type_rank(val: &Value) -> usize {
match val {
Value::String(s) => type_string_rank(s),
Value::Object(obj) => {
if let Some(Value::String(t)) = obj.get("type") {
type_string_rank(t)
} else {
100 }
}
_ => 100, }
}
fn type_string_rank(s: &str) -> usize {
match s {
"null" => 0,
"map" => 1,
"array" => 2,
"object" | "record" => 3,
"boolean" => 10,
"integer" | "int" | "long" => 11,
"number" | "float" | "double" => 12,
"enum" => 13,
"string" => 14,
"fixed" => 15,
"bytes" => 16,
_ => 99,
}
}
fn prepare_json_bytes<'a>(
json_bytes: &'a [u8],
index: usize,
config: &SchemaInferenceConfig,
) -> Result<Cow<'a, [u8]>, String> {
let json_str = std::str::from_utf8(json_bytes)
.map_err(|e| format!("Invalid UTF-8 at index {}: {}", index + 1, e))?;
if json_str.trim().is_empty() {
return Ok(Cow::Borrowed(&[]));
}
let validation_result = if let Some(delim) = config.delimiter {
if delim == b'\n' {
validate_ndjson(json_str)
} else {
Err(serde_json::Error::custom(format!(
"Unsupported delimiter: {:?}",
delim
)))
}
} else {
validate_json(json_str)
};
if let Err(parse_error) = validation_result {
let truncated_json = if json_str.len() > MAX_JSON_ERROR_LENGTH {
format!(
"{}... [truncated {} chars]",
&json_str[..MAX_JSON_ERROR_LENGTH],
json_str.len() - MAX_JSON_ERROR_LENGTH
)
} else {
json_str.to_string()
};
return Err(format!(
"Invalid JSON input at index {}: {} - JSON: {}",
index + 1,
parse_error,
truncated_json
));
}
if let Some(ref field) = config.wrap_root {
if config.delimiter == Some(b'\n') {
let mut wrapped_bytes = Vec::new();
for line in json_str.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let inner_val: Value = serde_json::from_str(trimmed)
.map_err(|e| format!("Failed to parse NDJSON line before wrap_root: {}", e))?;
if !wrapped_bytes.is_empty() {
wrapped_bytes.push(b'\n');
}
serde_json::to_writer(&mut wrapped_bytes, &json!({ field: inner_val }))
.map_err(|e| format!("Failed to serialize wrapped NDJSON: {}", e))?;
}
Ok(Cow::Owned(wrapped_bytes))
} else {
let inner_val: Value = serde_json::from_str(json_str)
.map_err(|e| format!("Failed to parse JSON before wrap_root: {}", e))?;
let wrapped_bytes = serde_json::to_vec(&json!({ field: inner_val }))
.map_err(|e| format!("Failed to serialize wrapped JSON: {}", e))?;
Ok(Cow::Owned(wrapped_bytes))
}
} else {
Ok(Cow::Borrowed(json_bytes))
}
}
fn process_json_strings_sequential<S: AsRef<str>>(
json_strings: &[S],
config: &SchemaInferenceConfig,
builder: &mut crate::genson_rs::SchemaBuilder,
) -> Result<usize, String> {
let build_config = BuildConfig {
delimiter: config.delimiter,
ignore_outer_array: config.ignore_outer_array,
};
let mut processed_count = 0;
for (i, json_str) in json_strings.iter().enumerate() {
profile_verbose!(config, "PROCESSING JSON STRING {}", i);
let prep_start = std::time::Instant::now();
let prepared_json = prepare_json_bytes(json_str.as_ref().as_bytes(), i, config)?;
let prep_elapsed = prep_start.elapsed();
profile_verbose!(config, " Preparation took: {:?}", prep_elapsed);
if prepared_json.is_empty() {
continue;
}
let mut bytes = prepared_json.into_owned();
let build_start = std::time::Instant::now();
let _schema = build_json_schema(builder, &mut bytes, &build_config);
let build_elapsed = build_start.elapsed();
profile_verbose!(config, " Schema building took: {:?}", build_elapsed);
processed_count += 1;
}
Ok(processed_count)
}
fn apply_force_field_types(schema: &mut Value, config: &SchemaInferenceConfig) {
match schema {
Value::Object(obj) => {
if let Some(props) = obj.get_mut("properties") {
if let Some(props_obj) = props.as_object_mut() {
for (field_name, field_schema) in props_obj.iter_mut() {
if let Some(forced) = config.force_field_types.get(field_name) {
if forced == "map" {
if let Some(field_obj) = field_schema.as_object_mut() {
field_obj.shift_remove("properties");
field_obj.shift_remove("required");
field_obj.insert("type".to_string(), json!("object"));
field_obj.insert(
"additionalProperties".to_string(),
json!({"type": "string"}),
);
}
}
}
apply_force_field_types(field_schema, config);
}
}
}
if let Some(items) = obj.get_mut("items") {
apply_force_field_types(items, config);
}
if let Some(additional) = obj.get_mut("additionalProperties") {
apply_force_field_types(additional, config);
}
if let Some(Value::Array(any_of)) = obj.get_mut("anyOf") {
for item in any_of {
apply_force_field_types(item, config);
}
}
}
Value::Array(arr) => {
for item in arr {
apply_force_field_types(item, config);
}
}
_ => {}
}
}
fn hash_schema(schema: &Value) -> u64 {
struct HashWriter(xxhash_rust::xxh64::Xxh64);
impl std::io::Write for HashWriter {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0.update(buf);
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let mut writer = HashWriter(xxhash_rust::xxh64::Xxh64::new(0));
serde_json::to_writer(&mut writer, schema).expect("hashing a Value cannot fail");
writer.0.digest()
}
fn process_json_strings_parallel<S: AsRef<str> + Sync>(
json_strings: &[S],
config: &SchemaInferenceConfig,
builder: &mut SchemaBuilder,
) -> Result<usize, String> {
profile!(
config,
"Starting parallel preparation and building ({})",
current_time_hms()
);
let chunk_size = config.max_builders.unwrap_or(json_strings.len());
if config.profile {
if let Some(rss) = get_rss_bytes() {
anstream::eprintln!("📊 RSS before parallel processing: {}", format_bytes(rss));
}
}
let mut processed_count = 0;
let mut seen_hashes = HashSet::new();
for (chunk_idx, chunk) in json_strings.chunks(chunk_size).enumerate() {
profile!(
config,
"Processing chunk {} ({} strings)",
chunk_idx,
chunk.len()
);
if config.profile {
if let Some(rss) = get_rss_bytes() {
anstream::eprintln!("📊 RSS before chunk {}: {}", chunk_idx, format_bytes(rss));
}
}
let t_par = std::time::Instant::now();
let chunk_builders: Vec<(usize, Option<(Value, u64)>)> = chunk
.par_iter()
.enumerate()
.map(
|(i, json_str)| -> Result<(usize, Option<(Value, u64)>), String> {
profile_verbose!(config, "Thread processing JSON STRING {}", i);
let prep_start = std::time::Instant::now();
let prepared = prepare_json_bytes(json_str.as_ref().as_bytes(), i, config)?;
let prep_elapsed = prep_start.elapsed();
profile_verbose!(
config,
" String {} preparation took: {:?}",
i,
prep_elapsed
);
if prepared.is_empty() {
return Ok((i, None));
}
let mut chunk_builder = get_builder(config.schema_uri.as_deref());
let mut bytes = prepared.into_owned();
let chunk_build_config = BuildConfig {
delimiter: config.delimiter,
ignore_outer_array: config.ignore_outer_array,
};
let build_start = std::time::Instant::now();
build_json_schema_into(&mut chunk_builder, &mut bytes, &chunk_build_config);
let build_elapsed = build_start.elapsed();
profile_verbose!(
config,
" String {} schema building took: {:?}",
i,
build_elapsed
);
let mut schema = chunk_builder.to_schema();
drop(chunk_builder);
if !config.force_field_types.is_empty() {
apply_force_field_types(&mut schema, config);
}
let hash = hash_schema(&schema);
Ok((i, Some((schema, hash))))
},
)
.collect::<Result<Vec<_>, String>>()?;
if config.profile {
if let Some(rss) = get_rss_bytes() {
anstream::eprintln!("📊 RSS after collecting chunk: {}", format_bytes(rss));
}
}
let par_el = t_par.elapsed();
let t_mrg = std::time::Instant::now();
let mut merged: Vec<Value> = Vec::with_capacity(chunk_builders.len());
for (_i, item) in chunk_builders {
let Some((schema, hash)) = item else {
continue;
};
if !seen_hashes.insert(hash) {
continue;
}
processed_count += 1;
merged.push(schema);
}
builder.add_schemas_mut(&mut merged);
merged.into_par_iter().for_each(drop);
profile!(
config,
"chunk {} parallel {:?} serial-merge {:?}",
chunk_idx,
par_el,
t_mrg.elapsed()
);
if config.profile {
if let Some(rss) = get_rss_bytes() {
anstream::eprintln!("📊 RSS after merging chunk: {}", format_bytes(rss));
}
}
}
profile!(config, "All chunks processed ({})", current_time_hms());
Ok(processed_count)
}
pub(crate) fn preprocess_force_field_types(schema: &mut Value, config: &SchemaInferenceConfig) {
match schema {
Value::Object(obj) => {
if let Some(props) = obj.get_mut("properties") {
if let Some(props_obj) = props.as_object_mut() {
for (field_name, field_schema) in props_obj.iter_mut() {
if let Some(forced) = config.force_field_types.get(field_name) {
if forced == "map" {
convert_to_map(field_schema);
}
}
preprocess_force_field_types(field_schema, config);
}
}
}
if let Some(items) = obj.get_mut("items") {
preprocess_force_field_types(items, config);
}
if let Some(additional) = obj.get_mut("additionalProperties") {
preprocess_force_field_types(additional, config);
}
if let Some(Value::Array(any_of)) = obj.get_mut("anyOf") {
for item in any_of {
preprocess_force_field_types(item, config);
}
}
}
Value::Array(arr) => {
for item in arr {
preprocess_force_field_types(item, config);
}
}
_ => {}
}
}
fn convert_to_map(schema: &mut Value) {
if let Value::Array(arr) = schema {
let has_null = arr.iter().any(|v| {
v == &Value::String("null".into())
|| v.get("type") == Some(&Value::String("null".into()))
});
if has_null {
*schema = serde_json::json!({
"type": ["null", "object"],
"additionalProperties": {"type": "string"}
});
} else {
*schema = serde_json::json!({
"type": "object",
"additionalProperties": {"type": "string"}
});
}
return;
}
if let Value::Object(obj) = schema {
if obj.contains_key("additionalProperties") {
return;
}
let is_nullable = obj
.get("type")
.and_then(|t| t.as_array())
.map(|arr| arr.contains(&Value::String("null".into())))
.unwrap_or(false);
obj.shift_remove("properties");
obj.shift_remove("required");
obj.shift_remove("anyOf");
if is_nullable {
obj.insert("type".to_string(), serde_json::json!(["null", "object"]));
} else {
obj.insert("type".to_string(), serde_json::json!("object"));
}
obj.insert(
"additionalProperties".to_string(),
serde_json::json!({"type": "string"}),
);
}
}
pub fn infer_json_schema_from_strings<S: AsRef<str> + Sync>(
json_strings: &[S],
config: SchemaInferenceConfig,
) -> Result<SchemaInferenceResult, String> {
profile!(
config,
"Processing {} strings ({})",
json_strings.len(),
current_time_hms()
);
debug!(config, "Schema inference config: {:#?}", config);
if json_strings.is_empty() {
return Err("No JSON strings provided".to_string());
}
let result = panic::catch_unwind(AssertUnwindSafe(
|| -> Result<SchemaInferenceResult, String> {
let mut builder = get_builder(config.schema_uri.as_deref());
profile!(config, "Starting preparation loop ({})", current_time_hms());
let use_parallel = std::env::var("GENSON_PARALLEL")
.map(|v| v == "1" || v.to_lowercase() == "true")
.unwrap_or_else(|_| json_strings.len() >= PARALLEL_THRESHOLD);
let processed_count = if use_parallel {
process_json_strings_parallel(json_strings, &config, &mut builder)?
} else {
process_json_strings_sequential(json_strings, &config, &mut builder)?
};
let mut final_schema = builder.to_schema();
profile!(
config,
"Applying force field types ({})",
current_time_hms()
);
preprocess_force_field_types(&mut final_schema, &config);
profile!(config, "Rewriting objects ({})", current_time_hms());
rewrite_objects(&mut final_schema, None, &config, true);
profile!(config, "Reordering unions ({})", current_time_hms());
reorder_unions(&mut final_schema);
#[cfg(feature = "avro")]
if config.avro {
let avro_schema = SchemaInferenceResult {
schema: final_schema.clone(),
processed_count,
}
.to_avro_schema(
"genson", Some(""),
Some(""), false, );
return Ok(SchemaInferenceResult {
schema: avro_schema,
processed_count,
});
}
Ok(SchemaInferenceResult {
schema: final_schema,
processed_count,
})
},
));
match result {
Ok(Ok(schema_result)) => Ok(schema_result),
Ok(Err(e)) => Err(e),
Err(_panic) => Err("JSON schema inference failed due to invalid JSON input".to_string()),
}
}
#[cfg(test)]
mod tests {
include!("tests/schema.rs");
}