npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@openapi-spec/downgrader

v0.8.0

Published

Downgrade OpenAPI specifications one minor version at a time: 3.2 to 3.1 and 3.1 to 3.0, for whole documents or individual schemas

Readme

@openapi-spec/downgrader downgrades OpenAPI Specification documents and Schema Objects one minor version at a time, 3.2 to 3.1 and 3.1 to 3.0, for tools that only support an older version, such as code generators, gateways, and validators. It is how oRPC generates 3.1 and 3.0 documents.

Downgrading loses detail, never meaning. Each step below lists what it converts and its limitations.

Usage

import {
  downgradeSchemaV31ToV30,
  downgradeSchemaV32ToV31,
  downgradeSpecV31ToV30,
  downgradeSpecV32ToV31,
} from '@openapi-spec/downgrader'

const v31 = downgradeSpecV32ToV31(v32Document)
const v30 = downgradeSpecV31ToV30(v31Document)

// There is no direct 3.2 to 3.0 converter on purpose. Compose the steps:
const downgraded = downgradeSpecV31ToV30(downgradeSpecV32ToV31(v32Document))

// Schema Objects convert on their own:
downgradeSchemaV31ToV30({ type: ['string', 'null'] })
// { type: 'string', nullable: true }

| Function | Input | Output | | ------------------------- | ------------------- | --------------------------------------- | | downgradeSpecV32ToV31 | 3.2 OpenAPIObject | 3.1 OpenAPIObject | | downgradeSchemaV32ToV31 | 3.2 SchemaObject | 3.1 SchemaObject | | downgradeSpecV31ToV30 | 3.1 OpenAPIObject | 3.0 OpenAPIObject | | downgradeSchemaV31ToV30 | 3.1 SchemaObject | 3.0 SchemaObject or ReferenceObject |

Each step can also be imported on its own from @openapi-spec/downgrader/v3.2-to-v3.1 or @openapi-spec/downgrader/v3.1-to-v3.0. All types come from @openapi-spec/types.

The output openapi is the latest patch release, 3.1.2 from downgradeSpecV32ToV31 and 3.0.4 from downgradeSpecV31ToV30. Set it afterwards for another patch release, as oRPC does.

The input is never changed, and the output shares no plain objects or arrays with it. Input is read as JSON would see it: an object counts whatever realm it comes from or null prototype it has, as in a document oRPC returns, and a key holding undefined, as spreading options often leaves, counts as missing. Other values, such as a Date, are kept as they are.

External $refs are left as written, except in a 3.2 content map, where 3.1 allows none and the entry becomes {}, which takes any payload. The files they point at are not converted, so bundle a multi-file document into one first.

Extensions pass through unconverted, so a $ref into one at the document root, in components, or right under a component schema, as Stoplight writes when it bundles files into x-ext, is replaced by its converted target.

3.2 → 3.1

Spec (downgradeSpecV32ToV31)

Schema Objects inside the document convert as in Schema.

| 3.2 | In 3.1 | | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | | $self | Removed. 3.1 resolves against the retrieval URL. | | jsonSchemaDialect naming the 3.2 dialect | The 3.1 dialect. | | components.mediaTypes | Removed. $refs to them in content are replaced by their targets. | | Server name | Removed. | | Tag summary, parent, and kind | Removed. A summary becomes the description when that is missing. | | Path Item query and additionalOperations | Removed. 3.1 has only the eight fixed methods. | | in: "querystring" parameters | Removed, with the $refs to them. | | allowReserved outside in: "query" | Removed. 3.1 applies it only to query parameters. | | style: "cookie" | Removed, leaving the form default. | | A path or cookie value that style: "cookie" or allowReserved sends as written | schema: {}. 3.1 percent-decodes it with the form or simple style instead. | | Parameter example and examples beside content | Removed. 3.1 allows them only beside schema. The media types keep their own examples. | | Parameter style, explode, and allowReserved beside content, which neither version allows | Removed. | | Media Type description | Removed. | | Media Type itemSchema | Alone, schema: { type: "array", items: … }. Beside schema, which describes the whole body, removed. | | Media Type prefixEncoding and itemEncoding, and Encoding Object encoding, prefixEncoding, and itemEncoding | Removed. 3.1 matches multipart parts only by property name, and does not nest encodings. | | Response summary | Removed. It becomes the description that 3.1 requires when that is missing. | | Example dataValue and serializedValue | Removed. Without value or externalValue, dataValue, or else serializedValue, fills value. | | OAuth deviceAuthorization flow | Removed. | | Security Scheme oauth2MetadataUrl and deprecated | Removed. | | Security requirement names other than a component name, such as a URI reference to a scheme | Removed, which asks for less. |

Limitations

  • Other $refs into a removed part, such as a schema $ref into components.mediaTypes, or into an itemSchema that moves into schema.items, and Links or discriminator mapping values that point at a removed operation, are left as written and dangle. A $ref by index to a parameter that comes after a removed querystring parameter in its list, such as #/paths/~1a/get/parameters/1/schema, resolves to the next parameter, or dangles when none follows.
  • A content $ref that does not resolve, such as an external one or one through $self, becomes {}, which takes any payload.
  • A security requirement that names its scheme by URI reference loses it, so 3.1 tools can read the operation as needing no credentials for it.
  • $self-relative references pass through unchanged.

Schema (downgradeSchemaV32ToV31)

Both versions build on JSON Schema 2020-12, so everything not listed here passes through.

| 3.2 | In 3.1 | | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------- | | XML nodeType | "attribute" becomes attribute: true, and "element" on an array becomes wrapped: true. Other values are removed. | | Discriminator defaultMapping | The whole discriminator is removed, since 3.1 has no default for a missing or unmapped value. |

Limitations

  • A $schema naming the 3.2 dialect passes through unchanged.
  • Older drafts are not supported. Subschemas under keywords that only draft-07 or 2019-09 define, such as definitions, pass through unconverted.

3.1 → 3.0

Spec (downgradeSpecV31ToV30)

Schema Objects inside the document convert as in Schema.

| 3.1 | In 3.0 | | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | jsonSchemaDialect | Removed. 3.0 has one fixed schema dialect. | | webhooks | Removed. $refs into them are replaced by their targets. | | components.pathItems | Removed. A Path Item $ref to one is merged: the target's fields fill in those the referencing Path Item lacks. Other $refs into them are replaced by their targets. | | A missing paths or Operation responses | Added, as {} and as a default response, since 3.0 requires them. | | Info summary and License identifier | Removed. An Info summary becomes the description when that is missing. | | Reference Object summary and description | Removed. A 3.0 Reference Object holds only $ref. | | mutualTLS security schemes | Removed, with the $refs to them and their names in security requirements. A requirement left empty stays and needs no security, since 3.0 cannot ask for mutual TLS at all. | | Security requirement scopes on apiKey and http schemes | []. 3.0 allows scopes only for OAuth2 and OpenID Connect. | | Encoding Object contentType beside style, explode, or allowReserved | */*. 3.1 ignores it there, also its default, and 3.0 ignores the others in multipart instead. |

Limitations

  • A target replacing several $refs stands in at each of them as the same object, so a Path Item referenced twice repeats its operationIds, and a YAML dump writes an alias. Where a target refers back to one it is being copied into, that inner reference becomes {}, or is removed where {} is not a valid value.
  • Links and discriminator mapping values that point into a removed part, and Links that name a webhook operation by operationId, are left as written.
  • Other Encoding Objects pass through, although 3.0 gives an untyped multipart part no application/octet-stream default.
  • A requirement that uses only mutual TLS becomes {}, so 3.0 tools read its operation as needing no security, and a requirement that pairs it with another scheme asks for that scheme alone.
  • A 3.1 document that already uses 3.2 fields, as FastAPI writes itemSchema for streaming responses, keeps them as written, and the schemas inside them are not converted. Run downgradeSpecV32ToV31 on it first.

Schema (downgradeSchemaV31ToV30)

Accepts the default OAS dialect, which adds discriminator, xml, externalDocs, and example to JSON Schema 2020-12. Every schema is read as 2020-12, whatever $schema or jsonSchemaDialect names.

| 3.1 | In 3.0 | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [type][js-type] with "null" | nullable: true beside the other type. type: "null" alone becomes type: "string", nullable: true, enum: [null], since 3.0 allows nullable only beside type, and code generators drop a bare enum: [null]. | | [type][js-type] with several types | anyOf with one branch per type, which takes items for "array", and properties, required, additionalProperties, minProperties, and maxProperties for "object". "null" gets its own branch, written as for type: "null" alone, since code generators ignore nullable inside an anyOf branch. | | An [anyOf][js-any-of] of { type: "null" } and one other branch, as Zod writes a nullable object, array, integer, enum, unknown, or $ref, Zod and Valibot a nullable nullish one with such a pair as the other branch, and Effect a nullish one with { type: "null" } twice | That branch with nullable: true in place of the anyOf when it has a single type, since code generators read the pair as a union. A branch with a $ref, an enum, allOf, anyOf, oneOf, or not stays, since nullable would not let null through those, and so does one that holds a type where the schema around it holds one, or another keyword that schema holds with another value, or holds properties or additionalProperties where that schema holds the other, since additionalProperties reads the properties beside it. A null branch whose other keywords repeat the schema around it, as ArkType writes the description of a described nullable value into every branch, or are its own title, description, example, or x- extensions, as Lithic and Mailgun document the null case, counts as { type: "null" }. When the pair merges, the null branch's own keywords are dropped, since they describe only null. | | Such an anyOf whose other branch stays | The pair becomes a oneOf when that branch has a type and no nullable, or is a $ref, alone or with siblings such as the description Zod writes beside a described reference, to such a schema or to one with no type that rejects null all the same, or is an allOf with such a member in any place, as Zod writes a nullable intersection, or is a union with no type whose branches each reject null, one of them without a scalar or "array" type, as Pydantic writes an Optional tagged union, and Valibot and Zod a nullable union, since null then matches one branch only, and NSwag reads that as nullable but the anyOf as an empty class. A pair with such an inline union that a $ref points to keeps the anyOf, since NSwag gives each $ref to a oneOf with null a type it never defines. So does a pair with a $ref to a oneOf with an object branch, inline or through a $ref, as Zod writes a discriminated union, and GitHub and Notion a tagged one, since NSwag writes a type it never defines for a $ref to such a oneOf, nullable or not, but an empty class for the anyOf. A schema with no type rejects null when it is an enum or const without null, as msgspec writes an Enum, an allOf with a member that has such a type or is a $ref to one, as OData writes a derived object, or a union whose branches each do, as Zod writes a registered union. | | Such an anyOf whose other branch is a $ref to a schema that already admits null, through a type that holds "null", as GitHub writes, or an anyOf with such a branch and only annotations beside it, as Zod and Pydantic write a nullable schema, or that holds only annotations, such as {} | The $ref alone, when nothing else sits beside the anyOf, as 3.0 writes a nullable reference. | | Such an anyOf whose other branch accepts every value, such as {}, which Zod writes for a nullable unknown and Pydantic for Optional[Any] | Removed, since the pair then accepts every value too, and code generators read it as a union with a string or as a type they never define. The branch's annotations, such as the description Zod writes there for a described unknown, take its place, unless the schema around it has one of them with another value. | | A [oneOf][js-one-of] of { type: "null" } and one other branch, as typia and Litestar write a nullable value | That branch with nullable: true in place of the oneOf, under the same conditions as the anyOf pair above but only with a single null branch and a branch without nullable, since null then matches one branch only. Otherwise the oneOf stays. | | An [anyOf][js-any-of] of scalar types, or of them and "array", as ArkType, Valibot and Pydantic write a union of them and Effect a number, and a [type][js-type] list of them, as Zod writes it and ASP.NET Core a number that may come as a string | A oneOf in place of the anyOf when each branch has a scalar or "array" type of its own and is not nullable, they do not name both "integer" and "number", and no enum or oneOf sits beside them, since no value then matches two branches, with null in it once where Effect writes it twice for a nullish number. Such a oneOf with nothing beside it but annotations, as a nullable union of them becomes, joins the branches around it, and its annotations, such as the description Zod writes there for a described union, move to the schema around it, unless that schema or another such branch has one of them with another value. NSwag reads that oneOf as the first branch, and the OpenAPI Generator TypeScript client as a union, but the anyOf as an empty class. A schema a $ref points to keeps the anyOf, since NSwag gives a property a type it never defines when an allOf holds a $ref to a oneOf, as a $ref with siblings becomes. | | [const][js-const] | enum with its one value, and the type of that value when it has none, since code generators drop or leave untyped a bare enum. const: null becomes the same schema as type: "null". A union whose branches all become enums of one type, as OpenSearch writes a documented enum, gets that type beside it too, since NSwag otherwise reads it as an empty class or as a type it never defines. When those enums hold nothing else, as Zod, Valibot, TypeBox and GitHub write a union of literals, or only keywords the schema around them holds with the same value, as ArkType writes a described one, they become one enum, beside any null branch, since code generators read the union as a string or as a union of one-value types. Beside branches of other types, as Zod, ArkType and Valibot write a union of string literals and number, and utoipa the unit variants of a Rust enum, the enums of each type become one, and a union of scalars then becomes a oneOf. A oneOf whose branches share a value stays. | | An [enum][js-enum] with no type, as ArkType writes a union of literals | The type its values other than null share, when they share one, with nullable beside a null, since code generators drop or leave untyped a bare enum, or read its integers as numbers, and kin-openapi rejects its null. | | Numeric [exclusiveMinimum][js-exclusive-minimum] and [exclusiveMaximum][js-exclusive-maximum] | minimum or maximum with a boolean exclusiveMinimum or exclusiveMaximum, unless minimum or maximum is stricter. | | [examples][js-examples] | The first entry fills example when that is missing. | | [prefixItems][js-prefix-items] | items matching any of the tuple's item schemas, and those of the items after them unless items: false or maxItems allows none, or {} when one of them accepts every value. That anyOf merges like the ones above, as for the key and value of a Zod Map entry. items: false becomes maxItems. | | [contentEncoding][js-content-encoding] and [contentMediaType][js-content-media-type] | format: "byte" for base64, and format: "binary" for binary or a media type without an encoding or a contentSchema, which marks structured text. It replaces a format that only repeats the encoding, such as Zod's format: "base64". Without a type, it gains only the format, so it still takes every value, unless it holds a $ref, allOf, anyOf, oneOf, properties, items or prefixItems, as a JSON body marked with its media type does. Then the keywords are removed. | | An [anyOf][js-any-of] with a branch that holds only contentMediaType, as Zod writes for a file with several media types | Removed, since that branch matches every value. | | An [anyOf][js-any-of] with branches that become equal, as oRPC 1.x writes a file with several media types, a string for each, and Effect a nullish union, with { type: "null" } twice | A branch equal to the one before it is removed, as the anyOf then matches the same values, and a lone branch left takes the place of the anyOf when the schema around it holds only annotations that the branch lacks or holds with the same value, or nothing else beside a $ref, so a nullable one then merges as above. NSwag and oapi-codegen read the anyOf of equal binary strings as a generated class or a union, not as a file upload. Branches are equal when each keyword holds the same value, or the same object rather than a copy. A oneOf keeps them, since it rejects a value that two branches match. | | A [$ref][js-ref] with siblings | allOf: [{ $ref }] beside the siblings, which 3.0 would ignore. Ones that only identify, such as $id, leave it bare. A branch of a oneOf or anyOf beside a discriminator that stays, with only annotations beside its $ref, such as the description the Anthropic and ElevenLabs APIs write for each, becomes the bare $ref instead, since 3.0 maps a discriminator only to the $refs among its branches, not to inline schemas, and oapi-codegen stops on the allOf there. So does a $ref with only annotations beside it to a schema whose discriminator stays, or to one whose allOf holds a $ref to such a schema, such as the description the OpenAI and Adyen APIs write beside a property holding a tagged union, or the examples the Anthropic API writes there, also beside a type the target holds too, as OpenAI writes for an input item, since 3.0 reads an allOf holding that $ref as one of the schemas the discriminator picks among. openapi-typescript then leaves out its tag, and Hey API pins the tag to the name of the schema around it when the target has no oneOf, as for Adyen's base with a mapping or the NPP API's GeoJSON point. In both cases the annotations beside the $ref are dropped, so a readOnly, writeOnly, default or deprecated there is lost, as for the readOnly the Cryptography Provider API writes beside one. | | [$defs][js-defs], and definitions, its draft-07 name | Removed. Each $ref into it, or into any items or additionalProperties, where the conversion can put another schema, is replaced by its converted target, where a reference back into a target being inlined becomes {}. | | A [discriminator][3.0-schema-discriminator] over $refs the conversion replaces, such as into $defs as Pydantic and msgspec write a tagged union | Removed. A discriminator picks only among named components, not the targets that replace those $refs, and its mapping into them would dangle, which validators that follow it, such as openapi-schema-validator, reject. | | Extensions in a [discriminator][3.0-schema-discriminator] | Removed. 3.0 allows none there. | | A [discriminator][3.0-schema-discriminator] beside a $ref or a type list, with no oneOf, anyOf, or allOf, as Pydantic writes for a single-member tagged union | Removed. It does nothing there, but 3.0 tools would apply it to the allOf or anyOf those become. | | Boolean subschemas