# Payload Validator

Validates JSON, YAML, XML and CSV payloads and reports every problem with a 1-based line and column, a stable rule code, and a fix hint. Goes past well-formedness to the failures that parse cleanly and still break: duplicate keys, integers that lose precision past 2^53, YAML values that mean different things under 1.1 and 1.2, ragged CSV rows, and XML doctypes that carry entity-expansion risk. Detects the format when the caller does not know it.

**Not for:** Not a schema validator — it checks that a payload is well-formed and free of silent-corruption hazards, not that it matches a JSON Schema, XSD, DTD or RelaxNG. Not a linter for style or key ordering, not a formatter, and not a converter between formats. Does not resolve XML external entities or fetch remote schemas by design, since doing so is the vulnerability it warns about. Payloads are validated in memory and never stored.

## Formats

- [Validate JSON](https://payload-validator.gumballtools.com/validate/json) — Reports every problem in one pass with a 1-based line and column, including the three that JSON.parse cannot report at all: duplicate keys, integer precision loss, and unpaired surrogates.
- [Validate YAML](https://payload-validator.gumballtools.com/validate/yaml) — Checks well-formedness, then reports the values that mean different things to different loaders — found by resolving each unquoted scalar under both spec versions and comparing, not by matching a list of suspicious words.
- [Validate XML](https://payload-validator.gumballtools.com/validate/xml) — Well-formedness plus the four classes of invalid XML that ordinary checkers accept, plus the entity-based attacks — reported, never resolved.
- [Validate CSV](https://payload-validator.gumballtools.com/validate/csv) — RFC 4180 scanning with the delimiter sniffed from the header. Ragged rows are reported per row with both field counts, because that is the entire answer.

## Surfaces

- HTML: `https://payload-validator.gumballtools.com`
- Markdown: same URLs with `Accept: text/markdown` or `?format=md`
- JSON API: `https://payload-validator.gumballtools.com/api/v1/validate`
- OpenAPI: `https://payload-validator.gumballtools.com/.well-known/openapi.json`
- MCP: `https://payload-validator.gumballtools.com/api/mcp` — tools: `validate_json`, `validate_yaml`, `validate_xml`, `validate_csv`, `validate_auto`

## Questions

### Why does it call my document invalid when it parses fine?

Because "parses" and "has one meaning" are different questions, and the result reports both separately. `parseable` tells you whether a conforming parser accepts the input. `valid` tells you whether the input is unambiguous. A JSON object with two identical keys is parseable and not valid: every parser accepts it, and they disagree about which value wins — JavaScript and Python keep the last, some Go and Rust decoders error. Calling that valid would defeat the point of the tool, and calling it unparseable would be false, so both booleans are returned and you can use whichever matches your question.

### What is the Norway problem?

In YAML 1.1, the unquoted words no, yes, on, off, y and n are booleans. So a list of ISO country codes containing `no` for Norway becomes ["se", false, "dk"] when read by PyYAML, which implements 1.1. YAML 1.2 dropped that rule, so Go's yaml.v3 and the yaml npm package read the same document as ["se", "no", "dk"]. Nothing in the file says which is correct — the answer depends entirely on the loader. Quoting the value pins it to a string, which is exactly what quoting means in YAML.

### Why does my 64-bit ID come back as a different number?

JSON numbers are IEEE-754 doubles in nearly every parser, which represent integers exactly only up to 9007199254740991, or 2^53-1. Above that, some integers are not representable and get rounded to the nearest one that is: 9007199254740993 parses as 9007199254740992. Snowflake IDs from Twitter and Discord, and most database bigints, are 64-bit and sit squarely in the lossy range. Nothing errors, so an ID can quietly come to point at a different row. It is why the Twitter API added id_str alongside id. The fix is to send large integers as strings. This validator proves the loss using exact BigInt arithmetic and shows you the value you would actually receive.

### Does it check my payload against a JSON Schema, XSD or DTD?

No. It checks that a payload is well-formed and free of silent-corruption hazards, not that it matches a schema. Those are genuinely different jobs: schema validation answers "does this document have the fields my code expects", and this answers "does this document mean one thing". A payload can satisfy a schema perfectly and still have a duplicate key that made the schema check pass by keeping the wrong value. It also never fetches a remote schema or DTD, because doing so is one of the vulnerabilities it reports.

### Is my payload stored anywhere?

No. Payloads are validated in memory and discarded when the response is sent. Nothing is written to a database, a log, or a cache. The site records that a call happened — the format, the timestamp, and a coarse caller class for quota accounting — but never the content. If you are validating something you cannot send to a third party at all, the engine is a pure function in an open-source repository and runs offline.

### Will it resolve an XML external entity to check whether it works?

No, and that is deliberate. The whole point of the XXE finding is that a parser which resolves external entities can be made to read local files or reach internal services. A validator that resolved them in order to report on them would be performing the attack rather than detecting it. Entity declarations are parsed and reported with the URI they point at; nothing is fetched. The same applies to alias bombs in YAML, which are detected by counting the expansion rather than performing it.

### Why does it guess my CSV delimiter, and can I stop it?

Because getting it wrong is silent. Wherever the comma is the decimal separator, CSV exports are semicolon-separated, and reading such a file as comma-separated produces one column of nonsense with no error at all. So the delimiter is sniffed from the header — ignoring quoted regions, so their contents cannot vote — and always reported in the stats, with a warning when the guess was a close call. Pass `delimiter` to remove the guess entirely.

### Why not just use JSON.parse in a try/catch?

It answers a narrower question than people think it does. JSON.parse cannot report duplicate keys, because the earlier value is gone before it returns. It cannot report integer precision loss, because the rounding has already happened. It reports one error and stops, so a document with six problems takes six round trips. And its message wording differs between V8, JavaScriptCore and SpiderMonkey, so you cannot branch on it. A scanner reports every finding in one pass, at an exact line and column, with a stable rule code you can assert on.

### What does the 1 MB limit mean in practice, and how do I send a large file?

The API accepts up to 1,000,000 bytes, measured as encoded bytes rather than characters, so a document of non-Latin text gets the limit it says. POST the file as the raw request body with any non-JSON content type and put the format in the query string: curl --data-binary @config.yaml '…/api/v1/validate?format=yaml'. The HTML form is capped much lower, at 6,000 characters, because a GET form puts the payload in the URL and requests over roughly 8 KB are rejected before any of our code runs. For a CSV larger than the limit, the first few thousand rows will surface a ragged-row problem just as well as all of them.
