ErrLookup › Background articles › ValidationError explained: why open-source libraries reject your input — file uploads, YAML manifests, unique fields, and query permissions
ValidationError explained: why open-source libraries reject your input — file uploads, YAML manifests, unique fields, and query permissions
ValidationError is the structured rejection libraries throw when input, configuration, or content fails a validation gate before it reaches the database or business logic. Developers meet it as a failed file upload (disallowed MIME type, unsafe SVG, corrupt PDF, extension/content mismatch), an invalid extension manifest (wrong YAML types, unsafe paths, bad command names), a duplicate value on a unique-indexed field, a query referencing a field the user's role cannot access, or a value that breaks a required grammar. This article covers the family across 13 repositories — Payload, Strapi, Spec Kit, Playwright, Django, and others — how the message shapes differ, and the fixes that hold everywhere.
Distilled from 175 documented records across 13 repositories.
Background
ValidationError sits at the trust boundary between a caller and a library's internals. It is raised deliberately at a validation gate, before data reaches the database or deeper logic: Strapi validates content-manager queries (filters, sort, populate) against field-level RBAC permissions before they hit the DB; Payload inspects uploaded file contents — magic-byte MIME detection, SVG sanitization, PDF integrity — inside checkFileRestrictions; Django's validate_password runs every configured AUTH_PASSWORD_VALIDATOR and aggregates the failures. The class exists to fail fast with a structured, nameable error instead of a downstream crash: Payload converts raw MongoDB E11000 duplicate-key errors and SQL UNIQUE-constraint failures into per-field ValidationErrors so the API returns a clean validation failure rather than a 500, and Spec Kit guards manifest fields up front precisely because non-string values would later raise bare TypeErrors that bypass its malformed-manifest handlers.
From the caller's side the error almost always names the offender. Spec Kit's messages interpolate the field and the actual type received ('expected a string, got float'); Strapi reports the offending key and its path within the query tree; dianping/cat's bundled CSS validator literally enumerates the accepted grammar ("Expected (<length> | <percentage> | inherit) but found '...'"); Django's aggregator collects every validator's message and code into one error_list. Several libraries join multiple sub-errors into a single message — Payload's file-restriction error concatenates all failed content checks with ', ' — so reading the full message tells you exactly which sub-check fired.
The family splits into two broad groups. One validates user-supplied data and content: upload MIME allowlists and extension/content cross-checks (Payload, ECC), uniqueness constraints (Payload's duplicate-key and upsert handlers, slug collisions), password policy (Django), and CSS value grammar (cat). The other validates configuration, metadata, and authority: Spec Kit's extension-manifest checks cover path safety, naming patterns, section shapes, and hook structure; Strapi's api-token assertions enforce that tokens are created by an authenticated, existing admin and owned by that admin; Playwright's variants are internal protocol-integrity checks (channel types in SocksSupport messages, dispatcher types in serialized results) where the error usually signals a library bug or a build/version mismatch rather than anything the caller did wrong.
Common causes
- Field value breaks a type or shape contract. A manifest or request field has the wrong type: unquoted YAML that coerces to float/int/bool (Spec Kit's version: 1.0, id: 2), an extension section that is a string or null instead of a mapping, hook entries that are bare strings instead of objects. The guards exist because the downstream regex match or Version parser would otherwise raise an unhandled TypeError.
- Duplicate value on a unique-indexed field. A create, update, or upsert sets a unique field (email, slug, username) to a value that already exists. Payload catches the MongoDB E11000 or SQL UNIQUE-constraint failure and re-throws it as a per-field 'Value must be unique' ValidationError; explicit user-supplied slugs are rejected on collision instead of being silently re-suffixed.
- Uploaded file content fails validation. The file's real content disagrees with what the configuration allows: magic-byte MIME not in the allowlist (ECC, Payload), detected MIME not matching upload.mimeTypes, extension/content mismatch such as a PDF renamed to .png, a truncated or corrupt PDF, an SVG containing scripts or external references, or an extension on the built-in restricted blocklist.
- Unsafe or malformed paths in manifests. A manifest file or alias field is an absolute path, contains '..' traversal, uses backslashes, has leading/trailing whitespace or a trailing directory slash, or names a drive letter or UNC path. Spec Kit's shared path-safety policy rejects all of these so extension files stay inside the extension directory.
- Query references a field the caller cannot access. A Strapi content-manager request filters, sorts on, populates, or requests an attribute the user's role lacks RBAC permission for, a hidden attribute, a password field, a relational field in a sort, or contains an empty-object filter value. The key and its path in the query tree are reported before the query reaches the database.
- Value does not match a required pattern or grammar. Command names outside the 'speckit.{extension}.{command}' namespace that auto-correction cannot salvage, CSS values that fit neither the keyword nor the type expression (flex: 1 2 3 4, display: foo), or passwords that fail configured validators (too short, too common, numeric, similar to user attributes).
- Missing or mismatched auth context and ownership. Strapi's api-token service rejects creates with no authenticated admin, an owner id that differs from the calling user, a calling user deleted mid-session, or non-custom tokens carrying a permissions array. Registration endpoints also reject missing, expired, or already-used registration tokens.
- Internal protocol or schema drift inside the library. Playwright's variants fire when a SocksSupport message carries a channel-typed field the schema does not expect, or a serialized Dispatcher's type does not match the declared channel slot. These usually indicate a Playwright source bug, an edited protocol.yml without regenerated channels, or mismatched client/server builds — not caller error.
What usually fixes it
- Read the entire message before changing anything. The error names the field, key, or path, often states the type actually received, and sometimes enumerates the accepted values verbatim (CSS grammar strings, joined sub-check lists). Aggregated errors (Django's error_list, Payload's joined message) list every failure at once.
- Correct the input to the declared contract rather than working around the validator: quote YAML values so they parse as strings, use relative forward-slash paths inside the expected directory, match required naming patterns exactly, and give sections the expected shape (mappings, not scalars).
- For upload failures, make the file's real content match the configuration: add the detected MIME to the allowlist, sanitize SVGs (strip scripts, on* handlers, external references), re-upload non-corrupt PDFs, or — only if you genuinely accept restricted types — use the library's explicit opt-out (e.g. Payload's allowRestrictedFileTypes) after weighing the security implications.
- For uniqueness failures, choose a different value, pre-check uniqueness before writing (with race tolerance), or let the server generate and auto-dedupe the value (e.g. omit an explicit slug); map the error's field path back to the form input so the user sees a field-level message.
- For authorization-flavored validation, fix the calling context: ensure the request runs authenticated, omit ownership fields so the controller sets them from the session, align requested fields with the role's RBAC permissions, and remove empty-object filters or relational sorts.
- When the error guards an internal invariant rather than your input — Playwright's channel/dispatcher checks — treat it as a library bug or version drift: align client and server builds, regenerate channels after protocol changes, and report stock-build occurrences instead of changing your test.
Go deeper
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Documented occurrences
- File type ${mimeTypeFromExtension} (from extension ${typeFromExtension}) is not allowed. / SVG file contains potentially harmful content. / Invalid or corrupted PDF file. / Invalid PDF file. / Invalid MIME type: ${detected.mime}. / File type '${file.mimetype}' not allowed ${file.name}: Restricted file type detected -- set 'allowRestrictedFileTypes' to true to skip this check for this Collection. (payloadcms/payload)
- Invalid command 'file' {label}: {reason} (github/spec-kit)
- Invalid key ${key} at ${path} (strapi/strapi)
- Invalid {singular} 'file' {label}: {reason} (github/spec-kit)
- Invalid extension.{field}: expected a string, got {type(ext[field]).__name__} (github/spec-kit)
- Invalid command name '{cmd['name']}': must follow pattern 'speckit.{extension}.{command}' (github/spec-kit)
- File type '${file.mimetype}' is not allowed. (payloadcms/payload)
- ${path}: channels are not expected in SocksSupport (microsoft/playwright)
- Invalid requires.speckit_version: expected a non-empty string, got {type(requires['speckit_version']).__name__} (github/spec-kit)
- Unsupported file type. (affaan-m/ECC)
- adminUserOwner must reference an existing admin user (strapi/strapi)
- Invalid registrationToken (strapi/strapi)
- adminUserOwner must match the authenticated admin user (strapi/strapi)
- Extension ID '{manifest.id}' conflicts with core command namespace '{manifest.id}' (github/spec-kit)
- Value must be unique (payloadcms/payload)
- File extension does not match file content. (affaan-m/ECC)
- Expected (none | [ <flex-grow> <flex-shrink>? || <flex-basis> ]) but found '{}'. (dianping/cat)
- Preserved extension config conflict for '{manifest.id}': The current config(s) ({names}) in {dest_dir} differ from their rescued backup in {rescue_staging_dir}. Both copies have been preserved. The config(s) ({names}) exist only in {dest_dir} with no counterpart in the rescued backup at {rescue_staging_dir}. Reconcile {dest_dir} and {rescue_staging_dir} to the desired final state, delete {rescue_staging_dir}, then reinstall. (github/spec-kit)
- Invalid extension: expected a mapping, got {type(ext).__name__} (github/spec-kit)
- Invalid command name: expected a string, got {type(cmd['name']).__name__} (github/spec-kit)
…and 155 more across the corpus — use search.
Honest provenance: generated on 2026-08-14 from AI-assisted analysis of the linked records. See how records are made.