Skip to content

vercel.json schema declares draft-04 but uses draft-06+ keywords, so strict validators reject it #17639

Open

Description

@simin-yuan

The schema at https://openapi.vercel.sh/vercel.json declares draft-04 but uses keywords that do not exist in draft-04, so it fails validation against the metaschema it names. Tools that honour $schema reject the document rather than applying it.

Reproduce

curl -sO https://openapi.vercel.sh/vercel.json
import json, jsonschema
s = json.load(open("vercel.json"))
print(s["$schema"])                      # http://json-schema.org/draft-04/schema#
jsonschema.Draft4Validator.check_schema(s)
jsonschema.exceptions.SchemaError: {'type': 'object', 'additionalProperties': False,
 'required': ['root'], 'properties': {'root': {'description': 'Path to the servic...

Behaviour across metaschemas (jsonschema 4.26.0), same file, unchanged:

metaschema result
draft-04 (the one it declares) REJECTED
draft-06 accepted
draft-07 accepted
2020-12 accepted

Keywords that force this

Counting occurrences in the served file:

keyword count introduced in
const 21 draft-06
if / then 15 / 15 draft-07
propertyNames 2 draft-06
exclusiveMinimum 3 boolean in draft-04, numeric from draft-06

exclusiveMinimum is the one worth pausing on: in draft-04 it is a boolean modifier on minimum; from draft-06 it is a number. It is not merely unrecognised here, it means something different.

Why it matters

Editors resolve this URL from $schema and from schema catalogues, and apply it while you type. A strict loader refuses the whole document at that point, which means no completion and no diagnostics — a config file with a typo looks exactly like a config file without one. Ajv in strictSchema mode is in the same position, and so is every Go/Java/Rust validator that reads $schema to pick its dialect.

Two things make this easy to miss from the inside: the file is consumed by Vercel's own tooling through a purpose-built reader (packages/config/scripts/generate-types.ts parses it by hand for code generation) rather than a general validator, and Vercel's TypeScript reader already assumes the newer keywords.

Suggested fix

Change the dialect declaration to the one the content already uses:

-  "$schema": "http://json-schema.org/draft-04/schema#",
+  "$schema": "http://json-schema.org/draft-07/schema#",

No other change is required. draft-07 accepts everything currently in the file, and it accepts the $data references only as opaque annotations, which is the same treatment they receive today. If the intent is to keep $data functional, note that it is an Ajv extension rather than a spec keyword, so a $comment saying so would help other consumers.

How this came up

I wrote a small mutation tester for validators — it feeds a validator inputs that ought to be rejected and reports what it lets through. Pointing it at served JSON schemas surfaced this one as a special case: the schema does not merely accept something it should reject, it cannot load at all under the dialect it names. The tool is https://github.com/simin-yuan/greencheck (MIT, no dependencies); the reproduction above stands on its own without it.

Activity

  1. simin-yuan commented on Sep 24, 2026

    @simin-yuan
    Author

    Verified independently, with a one-command repro.

    I re-ran this against a real validator instead of eyeballing it: check-jsonschema 0.38.0 / jsonschema 4.26.0, schema fetched from https://openapi.vercel.sh/vercel.json today.

    $ python -m check_jsonschema --schemafile "http://json-schema.org/draft-04/schema#" vercel.schema.json
    Schema validation errors were encountered.
      ... experimentalTriggers.items.oneOf[1].properties.retryAfterSeconds.exclusiveMinimum: 0 is not of type boolean
    $ echo $?
    1

    So yes, it fails against the metaschema it names. The failure is narrower than the title suggests, and worth stating precisely: only the 3 numeric exclusiveMinimum uses actually break draft-04, because draft-04 requires a boolean there. Same pointer three times, under functions, services.*.functions and experimentalServicesV2.*.functions.

    const (21 uses), if / then (15 each) and propertyNames (2) are also not draft-04 keywords. Those do not error, because a draft-04 validator simply ignores unknown keywords. They just silently do nothing for editors that trust the declared dialect, which is a weaker gap than the numeric exclusiveMinimum.

    Minimal fix, also verified: the schema has no id keywords that would need migrating, so switching the declared dialect is a one-line change.

    -  "$schema": "http://json-schema.org/draft-04/schema#",
    +  "$schema": "http://json-schema.org/draft-07/schema#",

    With that change the same command exits 0 against the draft-07 metaschema. Draft-07 covers every keyword used here.

    No affiliation with Vercel. Just a schema/validator mismatch I ran into while using this schema.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions