ParseErrors — Structured Error Accumulation
When a user submits a form with five invalid fields, they deserve to know about all five — not just the first one you found. ParseErrors is id_effect's solution: errors accumulate across an entire parse, and you report them all at once.
ParseError vs ParseErrors
#![allow(unused)] fn main() { use id_effect::schema::{ParseError, ParseErrors}; // One error let e: ParseError = ParseError::custom("age must be positive"); // Many errors let es: ParseErrors = ParseErrors::single(e); }
ParseError is a single failure. ParseErrors is a non-empty collection of failures with path information.
What a ParseError Contains
#![allow(unused)] fn main() { // A parse error has: // - a message // - a path (where in the data structure it occurred) // - optionally, the value that failed let err = ParseError::builder() .message("expected integer, got string") .path(["users", "0", "age"]) .received(Unknown::string("thirty")) .build(); println!("{err}"); // → users[0].age: expected integer, got string (received: "thirty") }
Path Tracking
Paths are built automatically as schemas descend into nested structures. You don't need to set them manually:
#![allow(unused)] fn main() { let raw = Unknown::from_json_str(r#" { "users": [ { "name": "Alice", "age": 30 }, { "name": "Bob", "age": "thirty" } ] } "#)?; let result = parse(users_schema, raw); // Err(ParseErrors { // errors: [ // ParseError { path: "users[1].age", message: "expected integer" } // ] // }) }
The struct_! macro and array combinator push path segments automatically. Custom schemas using .try_map or .filter inherit the current path.
Accumulation
The key property of ParseErrors is accumulation. When parsing a struct with multiple fields, failures from different fields are collected, not short-circuited:
#![allow(unused)] fn main() { let raw = Unknown::from_json_str(r#" { "name": "", "age": -5, "email": "not-an-email" } "#)?; let result: Result<User, ParseErrors> = parse(user_schema, raw); // Err(ParseErrors { // errors: [ // ParseError { path: "name", message: "must not be empty" }, // ParseError { path: "age", message: "age must be between 0 and 150" }, // ParseError { path: "email", message: "invalid email" }, // ] // }) }
All three errors reported in one call. No round-trips.
Using ParseErrors at API Boundaries
Convert ParseErrors to your API's error type:
#![allow(unused)] fn main() { #[derive(Debug)] enum ApiError { Validation(Vec<FieldError>), Internal(String), } #[derive(Debug)] struct FieldError { field: String, message: String, } fn to_api_errors(errs: ParseErrors) -> ApiError { ApiError::Validation( errs.into_iter() .map(|e| FieldError { field: e.path().to_string(), message: e.message().to_string(), }) .collect() ) } }
ParseErrors in Effects
parse returns a plain Result. To lift into an Effect:
#![allow(unused)] fn main() { effect! { let raw = Unknown::from_json_bytes(&body) .map_err(ApiError::InvalidJson)?; let req = parse(create_user_schema(), raw) .map_err(to_api_errors)?; ~ create_user(req) } }
The ? operator on a Result<T, ParseErrors> inside effect! maps the error into E via From. Define impl From<ParseErrors> for YourError to make this ergonomic.
Displaying ParseErrors
ParseErrors implements Display with a human-readable multiline format:
Validation failed (3 errors):
name: must not be empty
age: age must be between 0 and 150
email: invalid email
And Debug for the raw structure when inspecting in tests.
Summary
ParseError= one failure with a message and a pathParseErrors= all failures from a complete parse attempt- Paths are tracked automatically by schema combinators
- Accumulation means the user sees all problems at once
- Convert to your API error type at the boundary; keep the path information