use std::collections::{BTreeSet, HashSet};
use serde::de::{DeserializeSeed, Deserializer, Error as DeError, MapAccess, SeqAccess, Visitor};
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("unknown field `{field}` in `{object}`")]
pub struct UnknownField {
pub field: String,
pub object: &'static str,
}
pub fn check_members(
object: &'static str,
value: &serde_json::Value,
allowed: &[&str],
) -> Result<(), UnknownField> {
let Some(map) = value.as_object() else {
return Ok(());
};
let allowed: BTreeSet<&str> = allowed.iter().copied().collect();
for key in map.keys() {
if !allowed.contains(key.as_str()) {
return Err(UnknownField {
field: key.clone(),
object,
});
}
}
Ok(())
}
pub fn from_serde_error(e: &serde_json::Error) -> Option<String> {
let msg = e.to_string();
let rest = msg.strip_prefix("unknown field `")?;
let end = rest.find('`')?;
Some(rest[..end].to_string())
}
pub fn reject_duplicate_keys(bytes: &[u8]) -> Result<(), String> {
let mut de = serde_json::Deserializer::from_slice(bytes);
Deserializer::deserialize_any(&mut de, DuplicateKeyChecker).map_err(|e| e.to_string())
}
#[derive(Clone, Copy)]
struct DuplicateKeyChecker;
impl<'de> DeserializeSeed<'de> for DuplicateKeyChecker {
type Value = ();
fn deserialize<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(self)
}
}
impl<'de> Visitor<'de> for DuplicateKeyChecker {
type Value = ();
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("any JSON value")
}
fn visit_map<A>(self, mut map: A) -> Result<(), A::Error>
where
A: MapAccess<'de>,
{
let mut seen: HashSet<String> = HashSet::new();
while let Some(key) = map.next_key::<String>()? {
if !seen.insert(key.clone()) {
return Err(A::Error::custom(format!("duplicate field `{key}`")));
}
map.next_value_seed(DuplicateKeyChecker)?;
}
Ok(())
}
fn visit_seq<A>(self, mut seq: A) -> Result<(), A::Error>
where
A: SeqAccess<'de>,
{
while seq.next_element_seed(DuplicateKeyChecker)?.is_some() {}
Ok(())
}
fn visit_bool<E>(self, _v: bool) -> Result<(), E> {
Ok(())
}
fn visit_i64<E>(self, _v: i64) -> Result<(), E> {
Ok(())
}
fn visit_i128<E>(self, _v: i128) -> Result<(), E> {
Ok(())
}
fn visit_u64<E>(self, _v: u64) -> Result<(), E> {
Ok(())
}
fn visit_u128<E>(self, _v: u128) -> Result<(), E> {
Ok(())
}
fn visit_f64<E>(self, _v: f64) -> Result<(), E> {
Ok(())
}
fn visit_str<E>(self, _v: &str) -> Result<(), E> {
Ok(())
}
fn visit_bytes<E>(self, _v: &[u8]) -> Result<(), E> {
Ok(())
}
fn visit_char<E>(self, _v: char) -> Result<(), E> {
Ok(())
}
fn visit_none<E>(self) -> Result<(), E> {
Ok(())
}
fn visit_some<D>(self, deserializer: D) -> Result<(), D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_any(self)
}
fn visit_unit<E>(self) -> Result<(), E> {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde::Deserialize;
use serde_json::json;
#[test]
fn allows_known_members_and_ignores_extension_namespace_contents() {
let v = json!({"a": 1, "extensions": {"junk": 1}});
assert_eq!(check_members("x", &v, &["a", "extensions"]), Ok(()));
}
#[test]
fn rejects_a_member_outside_the_allowed_set() {
let v = json!({"a": 1, "b": 2});
let err = check_members("x", &v, &["a"]).unwrap_err();
assert_eq!(err.field, "b");
assert_eq!(err.object, "x");
}
#[test]
fn does_not_recurse_into_the_extension_namespace() {
let v = json!({
"extensions": {
"junk_key_of_any_shape": {"deeply": {"nested": true}},
}
});
assert_eq!(check_members("x", &v, &["extensions"]), Ok(()));
}
#[test]
fn non_object_value_is_ok() {
assert_eq!(check_members("x", &json!("a string"), &["a"]), Ok(()));
assert_eq!(check_members("x", &json!(null), &["a"]), Ok(()));
assert_eq!(check_members("x", &json!(42), &["a"]), Ok(()));
assert_eq!(check_members("x", &json!([1, 2, 3]), &["a"]), Ok(()));
}
#[test]
fn empty_object_with_no_allowed_members_is_ok() {
assert_eq!(check_members("x", &json!({}), &[]), Ok(()));
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
struct Closed {
#[allow(dead_code)]
a: i64,
}
#[test]
fn recovers_field_name_from_unknown_field_error() {
let err = serde_json::from_str::<Closed>(r#"{"a":1,"zzz":2}"#).unwrap_err();
assert_eq!(from_serde_error(&err), Some("zzz".to_string()));
}
#[test]
fn returns_none_for_missing_field_error() {
let err = serde_json::from_str::<Closed>(r#"{}"#).unwrap_err();
assert_eq!(from_serde_error(&err), None);
}
#[test]
fn returns_none_for_type_mismatch_error() {
let err = serde_json::from_str::<Closed>(r#"{"a":"not a number"}"#).unwrap_err();
assert_eq!(from_serde_error(&err), None);
}
#[test]
fn rejects_duplicate_key_at_top_level() {
let bytes = br#"{"version":"aitp/0.2","version":"aitp/0.3"}"#;
let err = reject_duplicate_keys(bytes).unwrap_err();
assert!(err.contains("duplicate field `version`"), "got: {err}");
}
#[test]
fn rejects_duplicate_key_in_nested_object() {
let bytes = br#"{"a": {"x": 1, "x": 2}}"#;
let err = reject_duplicate_keys(bytes).unwrap_err();
assert!(err.contains("duplicate field `x`"), "got: {err}");
}
#[test]
fn rejects_duplicate_key_in_object_nested_inside_array() {
let bytes = br#"{"entries": [{"jti": "a"}, {"x":1,"x":2}]}"#;
let err = reject_duplicate_keys(bytes).unwrap_err();
assert!(err.contains("duplicate field `x`"), "got: {err}");
}
#[test]
fn rejects_duplicate_key_several_levels_deep_inside_arrays_and_objects() {
let bytes = br#"{"entries": [{"cnf": {"jkt": "a", "jkt": "b"}}]}"#;
let err = reject_duplicate_keys(bytes).unwrap_err();
assert!(err.contains("duplicate field `jkt`"), "got: {err}");
}
#[test]
fn accepts_document_with_no_duplicates_at_any_level() {
let bytes = br#"{
"version": "aitp/0.2",
"sender": {"agent_id": "aid:pubkey:abc"},
"entries": [
{"jti": "a", "cnf": {"jkt": "x"}},
{"jti": "b", "cnf": {"jkt": "y"}}
],
"extensions": {"com.example/foo": {"a": 1, "b": 2}}
}"#;
assert_eq!(reject_duplicate_keys(bytes), Ok(()));
}
#[test]
fn bare_scalar_top_level_is_ok_not_this_functions_job() {
assert_eq!(reject_duplicate_keys(b"42"), Ok(()));
assert_eq!(reject_duplicate_keys(b"\"a string\""), Ok(()));
assert_eq!(reject_duplicate_keys(b"null"), Ok(()));
assert_eq!(reject_duplicate_keys(b"true"), Ok(()));
assert_eq!(reject_duplicate_keys(b"[1,2,3]"), Ok(()));
}
#[test]
fn malformed_non_duplicate_json_still_errs_but_not_as_a_duplicate() {
let err = reject_duplicate_keys(b"{not json").unwrap_err();
assert!(!err.contains("duplicate field"), "got: {err}");
}
#[test]
fn realistic_signed_fixture_has_no_false_positive() {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.parent()
.and_then(|p| p.parent())
.expect("repo root")
.join("tests/schemas/known-answer/signed-examples/session-bundle/kat-keypair-001-bundle.json");
let bytes = std::fs::read(&path).unwrap_or_else(|e| {
panic!("read {}: {e}", path.display());
});
assert_eq!(reject_duplicate_keys(&bytes), Ok(()));
}
}