jso 1.3.1

No-BS, no-bloat json library.
Documentation
#![allow(unused)]

#[derive(Debug)]
struct Person {
  name: String,
  age: u8,
  alive: bool,
}

const PEOPLE: [&str; 14] = [
  r#"{ "name": "Zen", "age": 10, "alive": true }"#,
  "",
  ";",
  "0",
  "{}",
  r#"{ "name": 0 }"#,
  r#"{ "name": "  " }"#,
  r#"{ "name": "Katya" }"#,
  r#"{ "name": "Katya", "age": null }"#,
  r#"{ "name": "Katya", "age": 0.1 }"#,
  r#"{ "name": "Katya", "age": -1 }"#,
  r#"{ "name": "Katya", "age": 8 }"#,
  r#"{ "name": "Katya", "age": 8, "alive": [] }"#,
  r#"{ "name": "Katya", "age": 8, "alive": false }"#,
];

fn main() {
  for json in PEOPLE {
    match parse_person(json) {
      Ok(p) => println!("{p:#?}"),
      Err(err) => eprintln!("failed to parse r#\"{json}\"#: {err:?}"),
    }
  }
}

#[derive(Debug)]
enum ParsePersonError {
  JsonParse(jso::parse::Error),
  JsonNotAnObject,
  Custom(&'static str),
}

fn parse_person(s: &str) -> Result<Person, ParsePersonError> {
  use ParsePersonError::*;

  let json = jso::parse(s).map_err(JsonParse)?;
  let mut o = json.obj().ok_or(JsonNotAnObject)?;

  let name = o
    .remove("name")
    .ok_or(Custom("missing \"name\""))?
    .str()
    .ok_or(Custom("\"name\" is not a string"))?
    .trim()
    .to_string();

  if name.is_empty() {
    return Err(Custom("\"name\" empty"));
  }

  let age = o
    .remove("age")
    .ok_or(Custom("missing \"age\""))?
    .num()
    .ok_or(Custom("\"age\" is not a number"))?;

  if age.fract() != 0. {
    return Err(Custom("\"age\" must be an integer"));
  }

  if !(0.0..=255.0).contains(&age) {
    return Err(Custom("\"age\" must be between 0-255"));
  }

  let age = age as u8;

  let alive = o
    .remove("alive")
    .ok_or(Custom("missing \"alive\""))?
    .bool()
    .ok_or(Custom("\"alive\" not a boolean"))?;

  Ok(Person { name, age, alive })
}