use serde_json::{json, Value};
#[derive(Debug, Clone)]
pub struct NormaliseConfig {
pub empty_as_null: bool,
pub coerce_string: bool,
}
impl Default for NormaliseConfig {
fn default() -> Self {
Self {
empty_as_null: true,
coerce_string: false,
}
}
}
pub fn normalise_value(value: Value, schema: &Value, cfg: &NormaliseConfig) -> Value {
match schema {
Value::String(t) if t == "string" => match value {
Value::Null => Value::Null,
v @ Value::String(_) => v,
v => Value::String(v.to_string()),
},
Value::String(t) if t == "int" || t == "long" => match value {
Value::Null => Value::Null,
Value::Number(n) if n.is_i64() => Value::Number(n),
Value::String(s) if cfg.coerce_string => {
s.parse::<i64>().map(|i| json!(i)).unwrap_or(Value::Null)
}
_ => Value::Null,
},
Value::String(t) if t == "double" || t == "float" => match value {
Value::Null => Value::Null,
Value::Number(n) if n.is_f64() => Value::Number(n),
Value::String(s) if cfg.coerce_string => {
s.parse::<f64>().map(|f| json!(f)).unwrap_or(Value::Null)
}
_ => Value::Null,
},
Value::String(t) if t == "boolean" => match value {
Value::Null => Value::Null,
Value::Bool(b) => Value::Bool(b),
Value::String(s) if cfg.coerce_string => match s.as_str() {
"true" | "1" => Value::Bool(true),
"false" | "0" => Value::Bool(false),
_ => Value::Null,
},
_ => Value::Null,
},
Value::Object(obj) if obj.get("type") == Some(&Value::String("record".into())) => {
let mut out = serde_json::Map::new();
if let Some(Value::Array(fields)) = obj.get("fields") {
for f in fields {
if let (Some(Value::String(name)), Some(field_schema)) =
(f.get("name"), f.get("type"))
{
let val = match &value {
Value::Object(m) => m.get(name).cloned().unwrap_or(Value::Null),
_ => Value::Null,
};
out.insert(name.clone(), normalise_value(val, field_schema, cfg));
}
}
}
Value::Object(out)
}
Value::Object(obj) if obj.get("type") == Some(&Value::String("array".into())) => {
let default_items = Value::String("string".into());
let items_schema = obj.get("items").unwrap_or(&default_items);
match value {
Value::Null => Value::Null,
Value::Array(arr) if arr.is_empty() && cfg.empty_as_null => Value::Null,
Value::Array(arr) => Value::Array(
arr.into_iter()
.map(|v| normalise_value(v, items_schema, cfg))
.collect(),
),
v => Value::Array(vec![normalise_value(v, items_schema, cfg)]),
}
}
Value::Object(obj) if obj.get("type") == Some(&Value::String("map".into())) => {
let default_values = Value::String("string".into());
let values_schema = obj.get("values").unwrap_or(&default_values);
match value {
Value::Null => Value::Null,
Value::Object(m) if m.is_empty() && cfg.empty_as_null => Value::Null,
Value::Object(m) => {
let mut out = serde_json::Map::new();
for (k, v) in m {
out.insert(k, normalise_value(v, values_schema, cfg));
}
Value::Object(out)
}
v => {
let mut out = serde_json::Map::new();
out.insert("default".into(), normalise_value(v, values_schema, cfg));
Value::Object(out)
}
}
}
Value::Array(types) => {
if types.iter().any(|t| t == "null") {
if value.is_null() {
Value::Null
} else {
let branch = types.iter().find(|t| *t != "null").unwrap();
normalise_value(value, branch, cfg)
}
} else {
normalise_value(value, &types[0], cfg)
}
}
_ => value,
}
}
pub fn normalise_values(values: Vec<Value>, schema: &Value, cfg: &NormaliseConfig) -> Vec<Value> {
values
.into_iter()
.map(|v| normalise_value(v, schema, cfg))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_normalise_record() {
let schema = json!({
"type": "record",
"name": "doc",
"fields": [
{"name": "id", "type": "string"},
{"name": "labels", "type": {"type": "map", "values": "string"}},
]
});
let cfg = NormaliseConfig::default();
let input = json!({"id": 42}); let normalised = normalise_value(input, &schema, &cfg);
assert_eq!(normalised, json!({"id": "42", "labels": Value::Null}));
}
#[test]
fn test_normalise_array_union() {
let schema = json!(["null", {"type": "array", "items": "string"}]);
let cfg = NormaliseConfig::default();
let input = json!("hello"); let normalised = normalise_value(input, &schema, &cfg);
assert_eq!(normalised, json!(["hello"]));
}
#[test]
fn test_empty_map_to_null() {
let schema = json!({"type": "map", "values": "string"});
let cfg = NormaliseConfig::default();
let input = json!({});
let normalised = normalise_value(input, &schema, &cfg);
assert_eq!(normalised, Value::Null);
}
#[test]
fn test_empty_map_preserved_if_flag_off() {
let schema = json!({"type": "map", "values": "string"});
let cfg = NormaliseConfig {
empty_as_null: false,
..NormaliseConfig::default()
};
let input = json!({});
let normalised = normalise_value(input, &schema, &cfg);
assert_eq!(normalised, json!({}));
}
#[test]
fn test_string_coercion_toggle() {
let schema = json!({
"type": "record",
"name": "doc",
"fields": [
{"name": "int_field", "type": "int"},
{"name": "bool_field", "type": "boolean"},
]
});
let input = json!({
"int_field": "42",
"bool_field": "true"
});
let cfg_no_coerce = NormaliseConfig {
empty_as_null: true,
coerce_string: false,
};
let norm_no_coerce = normalise_value(input.clone(), &schema, &cfg_no_coerce);
assert_eq!(
norm_no_coerce,
json!({
"int_field": Value::Null, "bool_field": Value::Null })
);
let cfg_coerce = NormaliseConfig {
empty_as_null: true,
coerce_string: true,
};
let norm_coerce = normalise_value(input, &schema, &cfg_coerce);
assert_eq!(
norm_coerce,
json!({
"int_field": 42,
"bool_field": true
})
);
}
}