{"id":49332056,"url":"https://github.com/aahoughton/oav","last_synced_at":"2026-05-04T07:05:13.111Z","repository":{"id":354010882,"uuid":"1214743588","full_name":"aahoughton/oav","owner":"aahoughton","description":"HTTP-aware OpenAPI 3.0/3.1/3.2 validator with JSON Schema 2020-12 codegen compiler","archived":false,"fork":false,"pushed_at":"2026-05-04T04:13:01.000Z","size":1276,"stargazers_count":0,"open_issues_count":10,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-04T04:17:26.090Z","etag":null,"topics":["json-schema","json-schema-validator","openapi","openapi-validator","openapi3","openapi31","typescript","validator"],"latest_commit_sha":null,"homepage":null,"language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aahoughton.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":"SECURITY.md","support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-19T01:42:27.000Z","updated_at":"2026-05-04T04:12:44.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/aahoughton/oav","commit_stats":null,"previous_names":["aahoughton/oav"],"tags_count":26,"template":false,"template_full_name":null,"purl":"pkg:github/aahoughton/oav","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aahoughton%2Foav","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aahoughton%2Foav/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aahoughton%2Foav/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aahoughton%2Foav/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aahoughton","download_url":"https://codeload.github.com/aahoughton/oav/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aahoughton%2Foav/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32597948,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-03T22:12:39.696Z","status":"online","status_checked_at":"2026-05-04T02:00:06.625Z","response_time":58,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["json-schema","json-schema-validator","openapi","openapi-validator","openapi3","openapi31","typescript","validator"],"created_at":"2026-04-26T23:01:44.253Z","updated_at":"2026-05-04T07:05:13.106Z","avatar_url":"https://github.com/aahoughton.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# oav\n\nOpenAPI **3.0** / **3.1** / **3.2** HTTP request and response\nvalidator for JavaScript and TypeScript services. Two primary drivers:\n\n- **Tenant overrides over a base spec.** When tenants extend a\n  shared API (adding a required header on one route, refining a\n  schema, requiring auth where the base spec didn't), they need to\n  document those changes in the spec they ship, not as\n  application-side patches. `applyOverlays` rewrites the document\n  at load time. Custom keywords, formats, and dialects plug into\n  the compiler the same way, so per-tenant validation rules don't\n  require forking. See [docs/overlays.md](./docs/overlays.md).\n- **Validators that fit in microservice runners.** `oav compile-spec\nopenapi.yaml` emits a single zero-dependency ES module exposing\n  the full validator surface. Targets Cloudflare Workers, Vercel\n  Edge, Lambda@Edge, Deno Deploy: runtimes where `new Function()`\n  is unavailable, or where dependency footprint matters. `--only\n\"POST /pets\"` (repeatable) scopes the output to specific\n  operations without touching the source spec.\n\nErrors come back as a typed tree (`code`, `path`, `message`,\n`params`, `children`). One validator call covers the full HTTP\nframe: method, path, parameters, body, content type, status, and\nheaders.\n\nIf you only need generic JSON Schema validation across many drafts,\nstart with Ajv. If you want a one-line Express middleware with file\nupload and auth handler conveniences built in, start with\n`express-openapi-validator`. See\n[docs/comparison.md](./docs/comparison.md) for the feature map.\n\n## Install\n\n`oav` is the package shorthand used throughout the docs. Install uses\nthe scoped npm package names shown below.\n\n| You need                                         | Package choice                  |\n| ------------------------------------------------ | ------------------------------- |\n| YAML specs, HTTP/YAML readers, and the `oav` CLI | `oav`                           |\n| JSON or pre-parsed specs with zero runtime deps  | `oav-core`                      |\n| Express 4 request middleware                     | `oav` + `oav-express4`          |\n| Express 5 request middleware                     | `oav` + `oav-express5`          |\n| Fastify `preValidation` hook                     | `oav` + `oav-fastify`           |\n| Edge/serverless validator emitted at build time  | `oav` as a dev/build dependency |\n\n`oav` is a superset of `oav-core`: same programmatic surface plus YAML\nreaders and the `oav` CLI. The CLI's `commander` and `esbuild` deps\nload on demand from the binary entry, never from the library\nentrypoints, so application bundles tree-shake them out.\n\n```bash\nnpm install @aahoughton/oav            # YAML + CLI\nnpm install @aahoughton/oav-core       # JSON only, zero runtime deps\nnpm install @aahoughton/oav-express4   # Express 4 adapter (transitively pulls oav-core)\nnpm install @aahoughton/oav-express5   # Express 5 adapter\nnpm install @aahoughton/oav-fastify    # Fastify adapter\n```\n\n`oav` re-exports `oav-core` at four subpath entrypoints (`/schema`,\n`/spec`, `/formats`, `/core`); on the lean package, substitute\n`oav-core` in imports that don't touch the YAML readers\n(`createYamlFileReader`, `createSmartHttpReader`) or the CLI. See\n[`docs/modules.md`](./docs/modules.md) for what each subpath exports.\n\n## Quick start\n\n### Express\n\n```ts\nimport express from \"express\";\nimport { createValidator, createYamlFileReader } from \"@aahoughton/oav\";\nimport { loadSpec } from \"@aahoughton/oav/spec\";\nimport { validateRequests } from \"@aahoughton/oav-express5\";\n\nconst { document } = await loadSpec({\n  reader: createYamlFileReader(),\n  entry: \"openapi.yaml\",\n});\nconst validator = createValidator(document);\n\nconst app = express();\napp.use(express.json());\napp.use(validateRequests(validator));\n\napp.post(\"/pets\", (req, res) =\u003e res.json({ ok: true }));\n```\n\nInvalid requests receive an `application/problem+json` response.\nValid requests continue to your route handlers. Express 4 uses the\nsame shape with `oav-express4`; Fastify uses `oav-fastify` as a\n`preValidation` hook. See [docs/integration.md](./docs/integration.md).\n\n### Framework-agnostic\n\n```ts\nimport { createValidator, createYamlFileReader, formatText } from \"@aahoughton/oav\";\nimport { loadSpec } from \"@aahoughton/oav/spec\";\n\nconst { document } = await loadSpec({\n  reader: createYamlFileReader(),\n  entry: \"openapi.yaml\",\n});\nconst validator = createValidator(document);\n\nconst err = validator.validateRequest({\n  method: \"POST\",\n  path: \"/pets\",\n  contentType: \"application/json\",\n  headers: { \"x-tenant\": \"acme\" },\n  body: { name: \"Fido\" },\n});\n\nif (err !== null) console.error(formatText(err));\n```\n\nFor a multi-file spec or a spec hosted over HTTP, compose readers:\n`composeReaders([createYamlFileReader(), createSmartHttpReader(), createFileReader()])`\nhandles local YAML, remote JSON / YAML, and local JSON transparently.\n\n`validateRequest` / `validateResponse` return `null` on success or a\n`ValidationError` tree on failure. Every error carries a stable `code`\n(e.g. `\"type\"`, `\"required\"`, `\"content-type\"`, `\"oneOf\"`), a `path`\nrooted at the HTTP frame (e.g. `[\"body\", \"pets\", 3, \"name\"]`), a\nhuman-readable `message`, and a machine-readable `params` object whose\nshape per code is documented in `BuiltInErrorParams`.\n\nRunnable end-to-end demos in [`examples/`](./examples/README.md):\ncustom formats, custom keywords, cross-field constraints, error\nbudgets, version differences, overlays, and spec-derived middleware\nconfig.\n\n## Where to go next\n\n| Task                                      | Read                                                                   |\n| ----------------------------------------- | ---------------------------------------------------------------------- |\n| Wire into Express, Fastify, Next.js, Hono | [docs/integration.md](./docs/integration.md)                           |\n| Patch a spec you do not own               | [docs/overlays.md](./docs/overlays.md)                                 |\n| Emit standalone validators                | [packages/cli/README.md](./packages/cli/README.md#compile-spec-output) |\n| Compare against Ajv and other tools       | [docs/comparison.md](./docs/comparison.md)                             |\n| Migrate from express-openapi-validator    | [docs/migration-from-eov.md](./docs/migration-from-eov.md)             |\n| Use custom formats, keywords, or limits   | [docs/configuration.md](./docs/configuration.md)                       |\n\n## How it compares\n\nThe JavaScript ecosystem already has solid OpenAPI validation tools:\nAjv for JSON Schema, `express-openapi-validator` for Express,\n`openapi-backend` for operationId routing plus validation, and smaller\nrequest/response validators for custom stacks. oav is aimed at\nHTTP-aware validation with structured errors, overlays, and standalone\nOpenAPI validator output. See [docs/comparison.md](./docs/comparison.md)\nfor the feature map, and [docs/migration-from-eov.md](./docs/migration-from-eov.md)\nif you are migrating from `express-openapi-validator`.\n\nNumbers below are from the [`performance/`](./performance/README.md)\nbenchmark on AWS c7i.large (Intel Sapphire Rapids, Node 22). Your\nhardware will vary.\n\n**Compile: oav is meaningfully faster.**\n\n|                                            | Ajv   | oav       |\n| ------------------------------------------ | ----- | --------- |\n| Single synthetic schema (varies by shape)  | ~6 ms | 25–200 µs |\n| Real-world spec (petstore-31, ~10 schemas) | 27 ms | 1.6 ms    |\n\nAjv compile is essentially constant overhead per schema; oav scales\nwith shape. The advantage shows up wherever validator construction\nsits in the hot path: per-request, per-tenant, per-test, edge\ncold-start, AOT module emit.\n\n**Validate: roughly tied on simple shapes; Ajv wins on complex.**\n\nBoth libraries are sub-microsecond per check on typical OpenAPI\nbodies. On complex `oneOf`/`allOf` or large arrays, Ajv leads by\n2–4× (say 100 ns to 400 ns per call, or 1.7 µs to 4 µs). oav's\n`predicate` mode (`compileSchema(..., { predicate: true })`) closes\nmost of that gap for yes/no use cases.\n\nFor typical HTTP workloads (1k–10k req/sec × ~1 validation per\nrequest), the difference is invisible at any of those numbers. For\nvalidation-heavy code (millions of validations per second), Ajv wins.\n\nFull per-shape breakdown: [`docs/comparison.md`](./docs/comparison.md). Raw\nbenchmark data and methodology:\n[`performance/README.md`](./performance/README.md).\n\n## Conformance\n\nThe [`conformance/`](./conformance/README.md) sub-package drives the\ncompiler and CLI against the upstream JSON Schema 2020-12 Test Suite,\na set of OpenAPI 3.0 / 3.1 / 3.2 petstore scenarios, and a handful of\nreal-world specs (Stripe, GitHub, DigitalOcean, Twilio, Asana, Box,\nAdyen) that have to load and compile without error. See\n[`conformance/REPORT.md`](./conformance/REPORT.md) for pass / fail\ncounts by category.\n\nCategories oav does not aim to cover:\n\n- `$dynamicRef` with runtime dynamic-scope rebinding (oav resolves\n  statically against the anchor map).\n- The `optional/format/*` subtree (`format` is annotation-only by\n  default per JSON Schema 2020-12 §6.3).\n- A small tail of isolated optional cases (float-overflow handling,\n  external-ref loading tied to dynamic scope).\n\nOpenAPI specs hand-authored or generated for typical APIs rarely\ntouch any of these. If they matter for your use case, the\n[report](./conformance/REPORT.md) lays out which tests fail and why.\n\n## CLI\n\n```bash\noav resolve openapi.yaml\noav validate openapi.yaml --request req.http\noav validate openapi.yaml --path \"POST /pets\" --body payload.json\noav validate openapi.yaml --path \"GET /pets\" --response --status 200 --body resp.json\noav compile-schema schema.json -o validator.mjs             # JSON Schema -\u003e standalone validator\noav compile-spec openapi.yaml  -o validator.mjs             # OpenAPI   -\u003e standalone HTTP validator (edge / Lambda)\n```\n\nFlags: `--format text|json|flat`, `--depth n`, `--overlay file`\n(repeatable), `-o file`, `--quiet`, `--dialect` (compile-schema /\ncompile-spec), `--requests-only` (compile-spec), `--only METHOD PATH`\n(compile-spec, repeatable). See\n[packages/cli/README.md](./packages/cli/README.md) for the full\nsurface, the `.http` file format, and both compile commands' output\ncontracts.\n\n## Versions\n\n`createValidator` reads the spec's `openapi` string once at construction\nand picks the matching dialect. No per-request branching.\n\n| Spec  | Dialect               | Notes                                                       |\n| ----- | --------------------- | ----------------------------------------------------------- |\n| 3.0.x | OAS 3.0 Schema Object | `nullable`, boolean `exclusiveMin/Max`, sibling-`$ref` drop |\n| 3.1.x | JSON Schema 2020-12   | Assertive `format`                                          |\n| 3.2.x | JSON Schema 2020-12   | Same as 3.1 + the `QUERY` HTTP method                       |\n\nOverride via `createValidator(spec, { dialect })` to force or customize\none of the built-in dialects (`jsonSchemaDialect`, `openapi31Dialect`,\n`oas30Dialect`). Unknown / missing `openapi` strings fall back to the\n3.1 dialect by default; configure with\n`onUnknownVersion: \"throw\" | \"warn\" | \"fallback31\"`.\n\n**Swagger 2.0 specs** aren't supported directly: `createValidator`\nthrows on `swagger: \"2.0\"` documents. Convert to OpenAPI 3.0 first with\n[`swagger2openapi`](https://github.com/Mermade/oas-kit/tree/main/packages/swagger2openapi)\nand pass the 3.0 output to `createValidator`:\n\n```bash\nnpx swagger2openapi swagger.json -o openapi.json\n```\n\n## Configuring the validator\n\n`createValidator(spec, options)` accepts options for dialect override,\ncustom formats and keywords, error budget, strict-mode lint, security\nshape-checking, ignored paths, and version-mismatch policy. See\n[`docs/configuration.md`](./docs/configuration.md) for the option\ntable, custom-keyword recipe, and bounded-error-collection details.\nThe canonical contract is the `ValidatorOptions` TSDoc.\n\n## Framework integration\n\n`oav` is a validator, not a middleware package: you write a short\nadapter between your framework and `validateRequest` /\n`validateResponse`, or use one of the companion adapter packages. An\ninline Express 5 adapter is about this long:\n\n```ts\nimport { allowHeaderFor, httpStatusFor, toProblemDetails } from \"@aahoughton/oav\";\n\napp.use(async (req, res, next) =\u003e {\n  const err = validator.validateRequest({\n    method: req.method,\n    path: req.path,\n    query: req.query as Record\u003cstring, string | string[]\u003e,\n    headers: req.headers as Record\u003cstring, string | string[]\u003e,\n    contentType: req.get(\"content-type\") ?? undefined,\n    body: req.body,\n  });\n  if (err === null) return next();\n  const allow = allowHeaderFor(err);\n  if (allow !== undefined) res.setHeader(\"Allow\", allow);\n  res\n    .status(httpStatusFor(err))\n    .type(\"application/problem+json\")\n    .json(toProblemDetails(err, { instance: req.originalUrl }));\n});\n```\n\nCompanion adapter packages cover common request-validation wiring:\n[`oav-express4`](./packages/oav-express4/README.md),\n[`oav-express5`](./packages/oav-express5/README.md), and\n[`oav-fastify`](./packages/oav-fastify/README.md). They share export\nnames and option shapes; only the framework type differs.\n\nFor Next.js, Hono, Bun, Deno, and custom frameworks, use the\nframework-agnostic `validateRequest` / `validateResponse` calls or the\nFetch helpers (`validateFetchRequest`, `validateFetchResponse`). See\n[docs/integration.md](./docs/integration.md) for body parsing,\nresponse validation, uploads, security, ignored paths, and custom error\nenvelopes.\n\n`oav` is not a drop-in replacement for `express-openapi-validator`.\nThe adapters cover request validation; response validation, auth\ndispatch, upload parsing, and custom error envelopes stay explicit in\nyour application. In return, the validator does not mutate `req` or\n`res`, OpenAPI 3.0 behavior is built into the dialect, and failures\ncome back as structured error trees rather than framework-specific\nerror classes.\n\n## Known limitations\n\nRuntime behavior corners. For feature-scope tradeoffs against Ajv and\nOpenAPI middleware packages (draft versions, `$data`, async\nvalidation, response interception, upload helpers), see\n[docs/comparison.md](./docs/comparison.md).\n\n- `$dynamicRef` behaves like `$ref` with anchor lookup; no runtime dynamic-scope traversal.\n- `style: deepObject` query parameters support only single-level nesting (`obj[key]=value`); OpenAPI 3.0–3.2 don't define nested semantics.\n- `pattern` keywords and `format: \"regex\"` compile to the JavaScript\n  built-in `RegExp`, which has no execution timeout. If your OpenAPI\n  spec is attacker-controlled (e.g. multi-tenant upload), a\n  catastrophic pattern like `(a+)+$` is a ReDoS vector against any\n  string the validator checks. Vet spec sources before loading them.\n  A pluggable `regexCompiler` option for plugging in `re2` or a\n  complexity-checking engine is tracked in\n  [#146](https://github.com/aahoughton/oav/issues/146).\n\n## Contributing\n\nSee [CONTRIBUTING.md](./CONTRIBUTING.md) for branch / PR / release flow.\nDevelopment workflow (lint / typecheck / test / build) and the\nconformance and performance sub-packages are described there and in\n[CLAUDE.md](./CLAUDE.md).\n\n## License\n\nMIT. See [LICENSE](./LICENSE).\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faahoughton%2Foav","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faahoughton%2Foav","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faahoughton%2Foav/lists"}