use std::{collections::HashSet, fmt};
use serde::de::{self, DeserializeOwned, DeserializeSeed, MapAccess, SeqAccess, Visitor};
mod by_name;
#[cfg(test)]
mod tests;
use by_name::ByName;
const EXAMPLE_TIME: &str = "2026-10-05T12:00:00Z";
const TIME_FAILURES: [&str; 7] = [
"input contains invalid characters",
"premature end of input",
"trailing input",
"input is out of range",
"no possible date and time matching input",
"input is not enough for unique date and time",
"bad or unsupported format string",
];
pub(crate) fn decode_strictly<T: DeserializeOwned>(body: &[u8]) -> Result<T, String> {
if body.iter().all(|byte| is_whitespace(*byte)) {
return Err("empty body".to_owned());
}
scan(body)?;
let mut deserializer = serde_json::Deserializer::from_slice(body);
let mut unknown = None;
let mut record = |path: serde_ignored::Path<'_>| {
if unknown.is_none() {
unknown = Some(last_name_of(&path));
}
};
let decoded: Result<T, _> = serde_path_to_error::deserialize(serde_ignored::Deserializer::new(
ByName(&mut deserializer),
&mut record,
));
if let Some(name) = unknown {
return Err(format!("unknown field {name:?}"));
}
decoded.map_err(|failure| describe(failure.path(), failure.inner()))
}
fn scan(body: &[u8]) -> Result<(), String> {
let mut deserializer = serde_json::Deserializer::from_slice(body);
let mut duplicate = None;
let scanned = Scan {
duplicate: &mut duplicate,
}
.deserialize(&mut deserializer);
if let Some(name) = duplicate {
return Err(format!("duplicate field {name:?}"));
}
match scanned {
Ok(()) => deserializer
.end()
.map_err(|_| "data after the JSON value".to_owned()),
Err(failure) if failure.to_string().starts_with("number out of range") => Ok(()),
Err(_) => Err("invalid JSON".to_owned()),
}
}
struct Scan<'d> {
duplicate: &'d mut Option<String>,
}
impl<'de> DeserializeSeed<'de> for Scan<'_> {
type Value = ();
fn deserialize<D: de::Deserializer<'de>>(self, deserializer: D) -> Result<(), D::Error> {
deserializer.deserialize_any(self)
}
}
impl<'de> Visitor<'de> for Scan<'_> {
type Value = ();
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("a JSON value")
}
fn visit_bool<E>(self, _: bool) -> Result<(), E> {
Ok(())
}
fn visit_i64<E>(self, _: i64) -> Result<(), E> {
Ok(())
}
fn visit_u64<E>(self, _: u64) -> Result<(), E> {
Ok(())
}
fn visit_f64<E>(self, _: f64) -> Result<(), E> {
Ok(())
}
fn visit_str<E>(self, _: &str) -> Result<(), E> {
Ok(())
}
fn visit_unit<E>(self) -> Result<(), E> {
Ok(())
}
fn visit_seq<A: SeqAccess<'de>>(self, mut items: A) -> Result<(), A::Error> {
while items
.next_element_seed(Scan {
duplicate: &mut *self.duplicate,
})?
.is_some()
{}
Ok(())
}
fn visit_map<A: MapAccess<'de>>(self, mut members: A) -> Result<(), A::Error> {
let mut names = HashSet::new();
while let Some(name) = members.next_key::<String>()? {
if !names.insert(name.clone()) {
*self.duplicate = Some(name);
return Err(de::Error::custom("a name occurs twice"));
}
members.next_value_seed(Scan {
duplicate: &mut *self.duplicate,
})?;
}
Ok(())
}
}
fn is_whitespace(byte: u8) -> bool {
matches!(byte, b' ' | b'\t' | b'\r' | b'\n')
}
fn last_name_of(path: &serde_ignored::Path<'_>) -> String {
match path {
serde_ignored::Path::Root => String::new(),
serde_ignored::Path::Map { key, .. } => key.clone(),
serde_ignored::Path::Seq { index, .. } => index.to_string(),
serde_ignored::Path::Some { parent }
| serde_ignored::Path::NewtypeStruct { parent }
| serde_ignored::Path::NewtypeVariant { parent } => last_name_of(parent),
}
}
fn names_in(path: &serde_path_to_error::Path) -> Vec<String> {
path.iter()
.filter_map(|segment| match segment {
serde_path_to_error::Segment::Map { key } => Some(key.clone()),
serde_path_to_error::Segment::Seq { index } => Some(index.to_string()),
serde_path_to_error::Segment::Enum { variant } => Some(variant.clone()),
serde_path_to_error::Segment::Unknown => None,
})
.collect()
}
fn subject_of(names: &[String]) -> String {
if names.is_empty() {
return "the body".to_owned();
}
format!("{:?}", names.join("."))
}
fn describe(path: &serde_path_to_error::Path, failure: &serde_json::Error) -> String {
let names = names_in(path);
let subject = subject_of(&names);
let reason = reason_of(failure);
if matches!(
path.iter().next_back(),
Some(serde_path_to_error::Segment::Unknown)
) {
return describe_key(&subject, &reason);
}
if let Some(field) = between_backticks(&reason, "missing field `") {
let mut names = names;
names.push(field.to_owned());
return format!("{} is missing", subject_of(&names));
}
if reason.starts_with("number out of range") {
return format!("{subject} is out of range");
}
if reason.starts_with("trailing characters") {
return format!("{subject} has too many elements");
}
if TIME_FAILURES.iter().any(|text| reason.starts_with(text)) {
return format!("{subject} must be a time such as {EXAMPLE_TIME:?}");
}
if let Some(variants) = reason.strip_prefix("unknown variant `") {
return format!("{subject} must be one of {}", variants_of(variants));
}
if let Some((unexpected, expected)) = reason
.strip_prefix("invalid type: ")
.and_then(|rest| rest.split_once(", expected "))
&& let Some(text) = describe_type(&subject, unexpected, expected)
{
return text;
}
if let Some((_, expected)) = reason
.strip_prefix("invalid value: ")
.and_then(|rest| rest.split_once(", expected "))
&& is_integer(expected)
{
return format!("{subject} is out of range");
}
if let Some((_, expected)) = reason
.strip_prefix("invalid length ")
.and_then(|rest| rest.split_once(", expected "))
&& let Some(text) = describe_length(&subject, expected)
{
return text;
}
if SERDE_FAILURES
.iter()
.any(|prefix| reason.starts_with(prefix))
{
return format!("{subject} can not be decoded");
}
reason
}
const SERDE_FAILURES: [&str; 7] = [
"invalid type: ",
"invalid value: ",
"invalid length ",
"unknown variant ",
"unknown field ",
"missing field ",
"duplicate field ",
];
fn describe_key(subject: &str, reason: &str) -> String {
if reason.contains("expected key to be a number") {
return format!("the keys of {subject} must be numbers");
}
let expected = reason
.split_once(", expected ")
.map_or("", |(_, expected)| expected);
if is_time(expected) || TIME_FAILURES.iter().any(|text| reason.starts_with(text)) {
return format!("the keys of {subject} must be times such as {EXAMPLE_TIME:?}");
}
match kind_of(expected) {
Some("a number") => format!("the keys of {subject} must be numbers"),
Some("a boolean") => format!("the keys of {subject} must be booleans"),
_ => format!("{subject} can not be decoded"),
}
}
fn is_time(expected: &str) -> bool {
expected.contains("formatted date and time string")
}
fn describe_type(subject: &str, unexpected: &str, expected: &str) -> Option<String> {
if is_time(expected) {
return Some(format!("{subject} must be a time such as {EXAMPLE_TIME:?}"));
}
if is_integer(expected) && unexpected.starts_with("floating point") {
return Some(format!("{subject} must be an integer"));
}
kind_of(expected).map(|kind| format!("{subject} must be {kind}"))
}
fn describe_length(subject: &str, expected: &str) -> Option<String> {
if expected.starts_with("struct ") {
return Some(format!("{subject} must be an object"));
}
let count = expected
.strip_prefix("a tuple of size ")
.or_else(|| expected.strip_prefix("an array of length "))
.or_else(|| {
expected.rsplit_once(" with ").map(|(_, count)| {
count
.trim_end_matches(" elements")
.trim_end_matches(" element")
})
})?
.parse::<usize>()
.ok()?;
Some(match count {
0 => format!("{subject} must have no elements"),
1 => format!("{subject} must have 1 element"),
count => format!("{subject} must have {count} elements"),
})
}
fn kind_of(expected: &str) -> Option<&'static str> {
if is_integer(expected) || matches!(expected, "f32" | "f64") {
return Some("a number");
}
if matches!(
expected,
"a string" | "a borrowed string" | "a character" | "string"
) {
return Some("a string");
}
if expected == "a boolean" {
return Some("a boolean");
}
if expected == "a sequence"
|| expected.starts_with("a tuple of size ")
|| expected.starts_with("an array of length ")
|| expected.starts_with("tuple struct ")
{
return Some("an array");
}
if expected == "a map"
|| expected.starts_with("struct ")
|| expected.starts_with("adjacently tagged enum ")
|| expected.starts_with("internally tagged enum ")
{
return Some("an object");
}
None
}
fn is_integer(expected: &str) -> bool {
matches!(
expected,
"u8" | "u16"
| "u32"
| "u64"
| "u128"
| "usize"
| "i8"
| "i16"
| "i32"
| "i64"
| "i128"
| "isize"
)
}
fn variants_of(text: &str) -> String {
let expected = text
.split_once("expected ")
.map_or("", |(_, expected)| expected);
let variants: Vec<String> = expected
.split('`')
.skip(1)
.step_by(2)
.map(|variant| format!("{variant:?}"))
.collect();
variants.join(", ")
}
fn between_backticks<'r>(text: &'r str, prefix: &str) -> Option<&'r str> {
text.strip_prefix(prefix)?.split('`').next()
}
fn reason_of(failure: &serde_json::Error) -> String {
let text = failure.to_string();
let position = format!(" at line {} column {}", failure.line(), failure.column());
text.strip_suffix(&position).unwrap_or(&text).to_owned()
}