# OpenAPI types: a guide to data types and formats (August 2026) An OpenAPI type declaration is not documentation. It is an instruction to a code generator. `type: string` produces a `string` in every target language and nothing else; `type: string, format: date-time` produces a `datetime` in Python, a `time.Time` in Go, and a parsed `Date` in TypeScript. A bare `type: object` produces an untyped bag that pushes every field access into runtime. The same spec that renders a reference page also determines whether a developer gets compile-time errors or production incidents, which is the axis this guide turns on: [model OpenAPI types for the artifacts generated from them](https://buildwithfern.com/), not for the validator that checks them. **TLDR:** - OpenAPI defines six data types (string, number, integer, boolean, array, object), with `null` added as a real type in 3.1. - `format` is an annotation, not a constraint. Tools may ignore any format they do not recognize, but SDK generators use it to pick native types. - OpenAPI 3.1 aligns fully with JSON Schema 2020-12, which removes `nullable`, changes `exclusiveMinimum`/`exclusiveMaximum` to numbers, and adds `const`, `prefixItems`, and `if`/`then`/`else`. - `oneOf` with a `discriminator` is the only composition keyword that reliably produces a type-safe union in generated code. `anyOf` usually degrades to a loose type. - Enums are the most common source of breaking changes, because adding a value breaks strictly typed clients that were compiled against the old set. - Fern reads OpenAPI 3.0 and 3.1, preserves discriminators in generated SDK models, and supports `x-fern-enum` for enum descriptions, custom names, and per-language casing. ## Where the OpenAPI type system comes from OpenAPI does not define its own type system. It borrows one from JSON Schema, and which version it borrows changed significantly between releases. OpenAPI 3.0 uses an extended subset of JSON Schema Specification Wright Draft 00. It adds keywords JSON Schema does not have (`nullable`, `discriminator`, `readOnly` semantics tied to requests and responses) and omits others. The practical consequence is that a 3.0 schema is not a valid JSON Schema document, so generic JSON Schema tooling cannot validate payloads against it without a translation step. [OpenAPI 3.1](https://learn.openapis.org/upgrading/v3.0-to-v3.1.html) closed that gap by adopting [JSON Schema 2020-12](https://json-schema.org/draft/2020-12/release-notes) wholesale. Schemas in a 3.1 document are JSON Schema documents. Anything valid in 2020-12 is valid in a 3.1 spec, including `$defs`, `unevaluatedProperties`, and conditional subschemas. OpenAPI 3.2.0, published in September 2025, builds on the same 3.1 schema foundation and adds features around streaming, hierarchical tags, and custom HTTP methods rather than changing the type model. The differences that bite during a 3.0 to 3.1 migration are concentrated in a handful of keywords: | Keyword | OpenAPI 3.0 | OpenAPI 3.1 | | --- | --- | --- | | Nullability | `nullable: true` alongside `type` | `type: [string, "null"]` | | Exclusive bounds | `exclusiveMinimum: true` plus `minimum` | `exclusiveMinimum: 10` (a number) | | Tuples | `items` accepting an array | `prefixItems` | | Examples | `example` (singular, one value) | `examples` (an array) | | Fixed value | Single-value `enum` | `const` | | File payloads | `type: string, format: binary` | Content-type based, `contentMediaType` | ## OpenAPI data types: the six primitives Every schema resolves to one of six base types, plus `null` in 3.1. The type alone is coarse; it is the combination of type, format, and constraints that generates useful code. | Type | JSON representation | Typical generated type | Notes | | --- | --- | --- | --- | | `string` | Text | `string`, `str` | The default carrier for dates, UUIDs, enums, and binary data | | `number` | Floating point | `float`, `float64`, `number` | Use `format: double` for 64-bit precision | | `integer` | Whole number | `int`, `int32`, `int64` | Not a JSON type; OpenAPI narrows `number` | | `boolean` | `true`/`false` | `bool`, `boolean` | Avoid tri-state booleans; use an enum | | `array` | Ordered list | `List`, `T[]` | `items` is required for a useful generated type | | `object` | Key-value map | Class, struct, interface | Without `properties`, this becomes an untyped map | | `null` | `null` | Optional/nullable wrapper | 3.1 only; 3.0 uses `nullable: true` | Two of these carry most of the risk. `object` without `properties` is the single most damaging declaration in a spec: it tells the generator nothing, so every consumer gets an untyped map and does field access by string key. Fern interprets a bare `type: object` as a record of string to unknown rather than a fully opaque `unknown`, which is narrower and more useful, but it is still a type the compiler cannot check. If the shape is known, declare it. `integer` is the second. JSON has no integer type, and JavaScript numbers lose precision above 2^53. An `int64` identifier serialized as a JSON number will silently round in a browser client. APIs that use 64-bit IDs should model them as `type: string, format: int64` or accept that some clients will corrupt them. ## OpenAPI formats: annotation, not enforcement `format` narrows the meaning of a primitive. The specification is explicit that support for any registered format is optional and that tools which do not recognize a format may fall back to the base type. It is an annotation with no guaranteed validation behavior, which is exactly why it matters for code generation: a generator that recognizes `date-time` emits a native date object, and one that does not emits a string. OpenAPI 3.0 defines a fixed list. OpenAPI 3.1 defers to the [OpenAPI Format Registry](https://spec.openapis.org/registry/format), which is open to formats defined by other specifications. | Format | Base type | Meaning | Typical generated type | | --- | --- | --- | --- | | `int32` | integer | 32-bit signed | `int`, `Integer` | | `int64` | integer | 64-bit signed | `long`, `int64` | | `float` | number | Single precision | `float` | | `double` | number | Double precision | `double`, `float64` | | `date` | string | RFC 3339 full-date | `date`, `LocalDate` | | `date-time` | string | RFC 3339 date-time | `datetime`, `time.Time` | | `uuid` | string | RFC 4122 UUID | `UUID` type where one exists | | `byte` | string | Base64-encoded | `bytes`, `byte[]` | | `binary` | string | Raw octets (3.0 file uploads) | `File`, stream, buffer | | `password` | string | Hint to mask in UIs | `string` | | `email`, `uri`, `hostname`, `ipv4`, `ipv6` | string | JSON Schema formats | `string` with validation | Two practical rules. First, annotate every string that is not free-form text. An unannotated timestamp arrives in the SDK as a string that every consumer parses by hand, differently. Second, do not invent formats and expect them to work; an unrecognized format is silently discarded by most tooling. Check what the generator supports instead: Fern, for example, recognizes `format: date-time-rfc-2822` as a distinct primitive for APIs that emit RFC 2822 timestamps rather than RFC 3339. ## Nullability, optionality, and the difference between them These are three separate concepts and conflating them produces the most confusing class of SDK bug. - **Optional** means the key may be absent from the payload. Controlled by omitting the property from `required`. - **Nullable** means the key may be present with the value `null`. Controlled by `nullable: true` in 3.0, or by including `"null"` in the type array in 3.1. - **Defaulted** means the server substitutes a value when the key is absent. Controlled by `default`, which is an annotation rather than an instruction to the server. ```yaml # OpenAPI 3.0 deleted_at: type: string format: date-time nullable: true # OpenAPI 3.1 deleted_at: type: [string, "null"] format: date-time ``` The distinction becomes load-bearing on `PATCH` endpoints, where "field absent" (leave unchanged) and "field null" (clear it) must be different wire representations. A generated SDK that models both as an optional nullable field cannot express the difference, so partial updates silently overwrite fields the caller never touched. Fern-generated SDKs distinguish the three states explicitly for `PATCH` operations, sending unset fields not at all, explicit nulls as `null`, and provided values normally. ## OpenAPI enum: modeling closed value sets An enum declares that a value is drawn from a fixed set: ```yaml Status: type: string enum: - pending - active - suspended ``` Generators turn this into a language-native enum, which gives developers autocomplete and exhaustive switch checking. That is the benefit. The cost is forward compatibility: when the API adds `archived`, every strictly typed client compiled against the old set either throws on deserialization or fails an exhaustiveness check. Adding an enum value is a breaking change for consumers even though it is additive on the server. Three mitigations are worth knowing: - **Model as an open enum.** Some generators emit a union of known values plus a string escape hatch, so unknown values deserialize rather than throw. This is the safer default for public APIs with evolving value sets, and it is one of the design choices that keeps [SDKs forward-compatible](https://buildwithfern.com/post/evergreen-sdks-developer-guide) as the API grows. - **Document values individually.** Plain OpenAPI has no place to describe what each enum value means. Fern's [`x-fern-enum` extension](https://buildwithfern.com/learn/api-definitions/openapi/extensions/enum-descriptions-and-names) adds per-value descriptions, deprecation flags, custom generated names for values containing symbols, and per-language casing overrides (snake_case, camelCase, SCREAMING_SNAKE_CASE, PascalCase). - **Use `const` for single-value cases.** In 3.1, a schema fixed to one value should use `const` rather than a one-element `enum`, which is clearer to both readers and tooling. Avoid boolean fields that later need a third state. `is_active: boolean` becomes a migration when the answer becomes "active, suspended, or pending". An enum from the start costs nothing extra. ## oneOf, anyOf, and allOf: composition and polymorphism The three composition keywords look interchangeable and are not. Their differences show up sharply in generated code. | Keyword | Validation meaning | Generated result | | --- | --- | --- | | `oneOf` | Matches exactly one subschema | Tagged union when a discriminator is present | | `anyOf` | Matches one or more subschemas | Usually an untagged union or a widened type | | `allOf` | Matches every subschema | Merged/composed type, often used for inheritance | | `not` | Matches no subschema | Rarely representable; usually dropped | **`oneOf` is the one that generates well**, and only when paired with a `discriminator`: ```yaml PaymentMethod: oneOf: - $ref: "#/components/schemas/Card" - $ref: "#/components/schemas/BankAccount" discriminator: propertyName: type mapping: card: "#/components/schemas/Card" bank_account: "#/components/schemas/BankAccount" ``` The `discriminator` tells a deserializer which variant to construct by inspecting one property, instead of attempting each subschema until one validates. Generators use it to emit language-native constructs: discriminated unions in TypeScript, sealed classes or the visitor pattern in Java, tagged enums in Rust. Fern preserves the discriminator field in generated SDK models rather than consuming it during deserialization, so application code can narrow the union on the discriminator value. Without a discriminator, a `oneOf` becomes an undiscriminated union, and the client has to guess by structural matching. That works when variants are structurally distinct and fails ambiguously when they overlap. A related irritation is sibling properties on the union schema, where common fields sit alongside the `oneOf`; Fern extracts those into shared base properties across variants rather than dropping them. **`anyOf` is usually a modeling mistake.** It most often appears where the author meant `oneOf` or meant nullability, and in 3.1 the nullability case is better expressed with a type array. Since "one or more" cannot be represented as a single native type, generators widen it, and the type safety intended by the spec disappears. **`allOf` composes.** It is the standard way to express a base schema plus extensions, and generators typically flatten it into a single type or map it to inheritance. The constraint worth remembering is that `allOf` is intersection, not override: a property redeclared in a second subschema must satisfy both declarations, so it cannot be used to loosen a base type. ## Constraints that generators actually use Validation keywords narrow the value space beyond the type. Some produce generated code, some produce documentation, and some produce neither. - **Strings:** `minLength`, `maxLength`, `pattern` (an ECMA-262 regex). Most generators render these in docs and enforce them at runtime only in languages with a validation layer. - **Numbers:** `minimum`, `maximum`, `multipleOf`, and in 3.1 the numeric `exclusiveMinimum`/`exclusiveMaximum`. - **Arrays:** `minItems`, `maxItems`, `uniqueItems`. `uniqueItems: true` maps to a set type in languages that have one. - **Objects:** `required`, `additionalProperties`, `minProperties`. Setting `additionalProperties: false` is the difference between a closed struct and an open map in several generators. - **Access:** `readOnly` marks a property that appears in responses but not requests; `writeOnly` is the inverse. Generators that honor these emit separate request and response models, which is usually what an API author wants and rarely what they get by default. Runtime validation is where these keywords stop being decorative. Fern SDKs include runtime validation through Pydantic in Python and Zod in TypeScript, so a response that violates the declared schema surfaces as a typed validation error at the boundary rather than as an attribute error three call frames later. ## How type precision changes the generated SDK The point of all of the above is downstream. The table below is the practical translation of loose typing into consumer pain. | Spec pattern | Generated result | Consumer cost | | --- | --- | --- | | `type: object` with no `properties` | Untyped map | No autocomplete, no compile-time checking | | Unannotated timestamp string | `string` | Every consumer writes their own parser | | `enum` with no open fallback | Closed native enum | New server values break old clients | | `oneOf` without `discriminator` | Structural union | Ambiguous deserialization | | `anyOf` across dissimilar types | Widened/loose type | Type safety lost entirely | | Missing `required` | Everything optional | Null checks on fields that always exist | | No `format` on integers | Default width | Precision loss on 64-bit values in JS clients | None of these are validator failures. A spec can be perfectly valid, lint clean, and still generate a client that a developer finds unpleasant to use. That is the case for reviewing schemas with the generated output in view, and for the broader [API design practices](https://buildwithfern.com/post/api-design-best-practices-guide) that treat the spec as a product surface rather than a description of one. At enterprise scale, where one specification feeds SDKs in many languages at once, the cost of a loose type multiplies across every target, which is a recurring theme in [OpenAPI code generation](https://buildwithfern.com/post/openapi-code-generation-enterprise). ## Common OpenAPI typing mistakes - **Inline schemas everywhere.** Anonymous inline objects generate machine-invented names like `GetUserResponseBodyData`. Move shared shapes into `components/schemas`, or override the generated name; Fern supports `x-fern-type-name` for this. - **Using `string` for money.** Model currency amounts as integer minor units with an explicit currency field, not as a float and not as an unconstrained string. - **Reusing one schema for request and response.** Server-assigned fields like `id` and `created_at` end up optional in requests or required in responses. Split the schemas, or use `readOnly`. - **Omitting error schemas.** Undocumented non-200 responses mean generated SDKs have no typed exceptions, so consumers parse error bodies by hand. - **Editing a generated spec by hand.** If the OpenAPI document is produced from server code, manual edits are overwritten on the next build. Use [overrides or overlays](https://buildwithfern.com/learn/api-definitions/openapi/overrides) to layer changes on top without touching the source. ## How Fern handles OpenAPI types Fern reads OpenAPI 3.0 and 3.1 directly and generates idiomatic, [type-safe SDKs](https://buildwithfern.com/learn/sdks/overview/how-it-works) in nine languages plus the interactive API reference from the same [API definition](https://buildwithfern.com/learn/api-definitions/openapi/overview). On the typing specifics above, that means discriminators preserved in generated models, `x-fern-enum` carrying per-value descriptions and casing into both code and docs, and `PATCH` operations that distinguish unset from explicit null. Where the source spec is machine-generated and cannot be edited, an overrides file layers those extensions on top so regeneration stays automated. ## Final thoughts on OpenAPI types and formats Type declarations in an OpenAPI document are generative instructions, and the quality of every SDK, code sample, and reference page downstream is bounded by their precision. A validator will accept `type: object` with no properties, an unannotated date string, and an `anyOf` that means nothing. A code generator will faithfully reproduce all three as a worse developer experience. Reviewing schemas against the generated output, rather than against the linter, is what closes that gap. [Book a demo](https://buildwithfern.com/book-demo) to see how Fern turns an OpenAPI specification into type-safe SDKs and an interactive API reference, with discriminated unions, enums, and formats mapped to native constructs in every language. ## FAQ ### What are the OpenAPI data types? OpenAPI defines six base types: `string`, `number`, `integer`, `boolean`, `array`, and `object`. OpenAPI 3.1 adds `null` as a real type, so nullability is expressed as a type array such as `type: [string, "null"]` instead of the `nullable: true` keyword used in 3.0. Each base type can be narrowed with a `format` and with validation keywords like `pattern`, `minimum`, or `required`. ### What is the difference between oneOf and anyOf in OpenAPI? `oneOf` requires a value to match exactly one subschema, while `anyOf` requires it to match at least one. The difference matters most in code generation: `oneOf` paired with a `discriminator` produces a tagged union with type-safe narrowing, whereas `anyOf` has no single native equivalent and is usually widened into a loose type. If the intent is "one of these shapes", use `oneOf` and add a discriminator. ### Are OpenAPI formats validated? Not necessarily. The specification states that support for any registered format is optional, and tools that do not recognize a format may fall back to the base type as if no format were specified. Formats are best understood as annotations that well-behaved generators use to select native types, such as mapping `format: date-time` to a `datetime` in Python or a `time.Time` in Go, rather than as validation guarantees. ### How do you document individual OpenAPI enum values? Base OpenAPI has no mechanism for describing individual enum values; the `description` applies to the whole schema. Fern's `x-fern-enum` extension fills that gap with per-value descriptions, a `deprecated` flag for retiring values without deprecating the enum, custom names for values containing symbols, and per-language casing overrides. Those descriptions carry into both the generated SDK code and the rendered API reference. ### Is adding a value to an OpenAPI enum a breaking change? For consumers, usually yes. Clients generated with a closed native enum will fail to deserialize an unrecognized value, and exhaustive switch statements stop compiling. Public APIs with evolving value sets should either generate open enums that tolerate unknown values or treat enum additions as versioned changes with advance notice. Modeling the field as a constrained string rather than an enum trades away autocomplete to avoid the problem entirely.