Validation and Schema - dathere/qsv GitHub Wiki
Tier: Intermediate
Commands covered: validate, schema, sniff (see also profile for standards-based metadata generation)
Note
Per-command flag reference lives in /docs/help/. This page is the workflow layer β when to reach for each command and how they compose.
This is qsv's data quality story. Use schema to infer a JSON Schema 2020-12 spec from representative data, edit the schema by hand to tighten the rules, then validate to enforce it against new data at up to 780,000 rows/sec. Use sniff for fast metadata (delimiter, types, MIME) when you don't need a full schema scan.
For the deep-dive, see docs/Validate.md.
| If you want to⦠| Use | Notes |
|---|---|---|
| Check a CSV is valid RFC 4180 + UTF-8 |
validate (no schema) |
Run before commands that assume well-formed input (especially stats) |
| Validate against a JSON Schema 2020-12 spec | validate <data.csv> <schema.json> |
Outputs .valid.csv, .invalid.csv, .validation-errors.tsv
|
| Generate a JSON Schema from representative data | schema |
.schema.json sidecar; edit to tighten rules |
| Get a JSON Schema for a SAS / Stata / SPSS file from its own metadata | readstat --dictionary |
Variable & value labels, declared missing values; enum only when labels cover every value, so validate never rejects the data it describes. No LLM (24.0.0+) |
Generate a Polars schema for sqlp / joinp / pivotp
|
schema --polars |
.pschema.json sidecar; speeds up Polars commands |
| Quickly probe an unknown CSV / URL | sniff |
Faster than schema; samples the first N rows |
| Generate standards-based metadata (DCAT / Croissant / CKAN) | profile |
.metadata.json; FAIRification for data portals & ML registries |
JSON Schema Validation Draft 2020-12 with the fastest validator in Rust (jsonschema-rs). Up to 780k rows/sec on NYC 311 with the bundled schema.
Without a schema argument, validate just verifies RFC 4180 conformance and UTF-8 β a cheap sanity check before running stats (which assumes well-formed input).
With a schema, three output files are produced:
-
<input>.valid.csvβ rows that passed -
<input>.invalid.csvβ rows that failed -
<input>.validation-errors.tsvβrow_number, field, errorfor every failure
One custom format and two custom keywords beyond stock JSON Schema 2020-12:
-
"format": "currency"β ISO 4217 currency validation (USD,β¬,Β₯,$1,000.00,(β¬100,00),-USD100.00, β¦). A custom format, not a keyword. -
dynamicEnum(keyword) βenumwhose values come from a CSV (local, HTTP/HTTPS,dathere://, orckan://). Use this when the valid-value set is too large to hardcode or it changes over time. -
uniqueCombinedWith(keyword) β composite-key uniqueness check across columns.
Example: cheap RFC 4180 + UTF-8 sanity check
qsv validate NYC_311_SR_2010-2020-sample-1M.csv
# Exit code 0 = valid; non-zero = problem; details to stderr.Example: quarantine ragged rows instead of aborting
Without a schema, validate normally aborts on the first "ragged" row β one with the wrong number of columns. --split-ragged is an opt-in mode that keeps going and separates the file instead:
qsv validate --split-ragged data.csv
# data.csv.valid β the well-formed rows
# data.csv.invalid β the ragged rows
# data.csv.validation-errors.tsv β a report, appendedIt returns a non-zero exit code when any ragged row is found, so it still fails a pipeline while handing you both halves of the file. Only column-count mismatches are diverted β malformed quoting and non-UTF-8 data still abort.
validate has no parser-config options of its own, but it honors the global QSV_COMMENT_CHAR in both modes. Commented lines (including a commented header) are skipped and are not written to the .valid file:
QSV_COMMENT_CHAR='#' qsv validate --split-ragged data.tsvExample: validate NYC 311 against the bundled schema
qsv validate \
NYC_311_SR_2010-2020-sample-1M.csv \
resources/test/311_Service_Requests_from_2010_to_Present-2022-03-04.csv.schema.json
ls NYC_311_SR_2010-2020-sample-1M.*
# .valid.csv .invalid.csv .validation-errors.tsvExample: dynamicEnum against a CSV of valid NYC agencies
{
"type": "object",
"properties": {
"Agency": {
"type": "string",
"dynamicEnum": "NYC_agencies.csv"
}
}
}dynamicEnum accepts a local path or a URL β including ckan:// to fetch a resource by CKAN ID.
Example: uniqueCombinedWith for a composite primary key
{
"properties": {
"Created Date": { "uniqueCombinedWith": ["Complaint Type", "Incident Address"] }
}
}This enforces that the combination of those three columns is unique across the file.
Example: currency format validation
{
"properties": {
"Sale Price": { "type": "string", "format": "currency" }
}
}Example: validate a directory of CSVs against one schema
qsv validate exports/2024/ schema.jsonExample: fancy regex (look-around, backreferences) when needed
qsv validate --fancy-regex data.csv strict-schema.jsonBy default validate uses the same fast regex crate as search. --fancy-regex opts into the slower but more expressive fancy-regex crate for schemas that need it.
See also: /docs/help/validate.md, docs/Validate.md β deep dive, schema, Lookup Tables β for dynamicEnum patterns, Recipe: JSON Schema Validation.
Two modes:
-
JSON Schema mode (default) β produces a
<input>.schema.jsonwith inferred types, formats, ranges, and enum / pattern constraints. Edit the file by hand to tighten rules (mark required fields, adddynamicEnumlookups, set customcurrencyformats, β¦) before using it withvalidate. -
Polars Schema mode (
--polars) β produces a<input>.pschema.jsonconsumed bysqlp/joinp/pivotpto skip type inference. Edit to change inferred Polars data types where needed.
Both reuse the stats cache when available (run qsv stats --cardinality --infer-dates --stats-jsonl <input> first for max speed).
Example: infer a JSON Schema from NYC 311
qsv stats --cardinality --infer-dates --infer-boolean --stats-jsonl \
NYC_311_SR_2010-2020-sample-1M.csv
qsv schema NYC_311_SR_2010-2020-sample-1M.csv
# Produces NYC_311_SR_2010-2020-sample-1M.csv.schema.jsonThe generated schema has inferred types, min/max ranges (for numerics), date formats, enum values (for low-cardinality columns), regex patterns (when --pattern-columns is used), and the qsv-specific currency, dynamicEnum, uniqueCombinedWith custom keywords where appropriate.
--enum-thresholdis applied consistently (Unreleased, after 25.0.0, #4830). Which columns gotenum,constandformatconstraints used to depend on the other columns in the file. With no column at or under--enum-threshold, every column got an exhaustiveenum. With at least one, single-value columns lost theirconst, and--strict-formatsskipped high-cardinality String columns, so an email column with many distinct values got noformat: email. Now only columns at or under the threshold get anenum(with--ignore-case, String columns are counted case-insensitively), every single-value column getsconst, and--strict-formatschecks every String column.Generated schemas can therefore be stricter. A
constlocks a column to the one value it held in the input, sovalidaterejects later data where it changes (a new phone number or URL, say). Generate the schema from a representative dataset, then loosen or removeconst/enumconstraints where values are expected to change.--strict-formatsnow keeps every distinct value of each String column in memory while it checks them, which costs more on large files with many distinct text values.
--ignore-caseschemas validate their own data (Unreleased, after 25.0.0, #4831).--ignore-caselowercases the values it collects, andvalidateis case-sensitive, so a String column'senum/const(e.g.["false","true"]) used to reject the source'sTrue/TRUE. With--ignore-case,schemanow writes them as an equivalent case-insensitive regex instead, e.g."pattern": "^(?i:false|true)$". Values are escaped so that.,|,(etc. match literally, and the TurkishΔ°is accepted alongside theiΜit lowercases to. Integer enums stay enums.The inline
(?i:β¦)modifier is regex syntax that qsv'svalidateunderstands, as do recent JavaScript engines (tested with AJV 8 on Node 26). Other or older JSON Schema validators may not support it. If a schema must work outside qsv, generate it without--ignore-case.
Example: re-validate the source data β schema should always pass for the data it was inferred from
qsv validate \
NYC_311_SR_2010-2020-sample-1M.csv \
NYC_311_SR_2010-2020-sample-1M.csv.schema.json
# Exit code 0; no .invalid.csv or .validation-errors.tsv producedExample: Polars schema for speeding up sqlp
qsv schema --polars NYC_311_SR_2010-2020-sample-1M.csv
# Produces NYC_311_SR_2010-2020-sample-1M.pschema.json
# Now sqlp / joinp / pivotp pick it up automatically
qsv sqlp NYC_311_SR_2010-2020-sample-1M.csv \
"SELECT Borough, COUNT(*) FROM nyc311 GROUP BY Borough"Example: validate a manually-fine-tuned schema is itself valid JSON Schema
qsv validate schema fine-tuned-schema.jsonSee also: /docs/help/schema.md, validate, JSON Schema 2020-12 spec, stats, Stats Cache & Caching.
(Also covered in Selection & Inspection β sniff. Mentioned here because it's the lightweight cousin of schema for validation workflows.)
sniff samples the first N rows (default 1,000) to infer delimiter, header row, preamble rows, quote character, encoding, average record length, total row count, and per-field types & names. Works on local files and remote URLs (without downloading the whole file).
With the magika feature compiled in, it also identifies 200+ file types via Google's AI-based content detector β useful for CKAN harvesting.
Example: probe a remote dataset before downloading
qsv sniff https://opendata.cityofnewyork.us/data.csv
# Reports delimiter, headers, content length, estimated row count, field typesExample: MIME-type-only probe for fast URL health checks (CKAN-style)
qsv sniff --no-infer https://example.com/data.xlsxExample: stricter sampling for an oddly-shaped CSV
qsv sniff --sample 0.20 --delimiter '|' weird.csvFor guaranteed (non-sampled) type inference, use stats or schema β they scan the entire file.
- Command Reference (index)
-
Aggregation & Statistics β stats β guaranteed type inference (vs.
sniff's sample) -
SQL & Polars β
schema --polarsproduces the.pschema.jsonconsumed here -
Metadata Profiling (
profile) β standards-based metadata generation & validation (DCAT / Croissant / CKAN) -
Lookup Tables β
dynamicEnumagainst local files, HTTP URLs, dathere, CKAN -
docs/Validate.mdβ canonical reference for custom keywords - Cookbook β JSON Schema Validation
- Cookbook β Clean & Normalize