OpenAPI types: a guide to data types and formats (August 2026)

13 min read

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, 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 closed that gap by adopting JSON Schema 2020-12 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:

KeywordOpenAPI 3.0OpenAPI 3.1
Nullabilitynullable: true alongside typetype: [string, "null"]
Exclusive boundsexclusiveMinimum: true plus minimumexclusiveMinimum: 10 (a number)
Tuplesitems accepting an arrayprefixItems
Examplesexample (singular, one value)examples (an array)
Fixed valueSingle-value enumconst
File payloadstype: string, format: binaryContent-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.

TypeJSON representationTypical generated typeNotes
stringTextstring, strThe default carrier for dates, UUIDs, enums, and binary data
numberFloating pointfloat, float64, numberUse format: double for 64-bit precision
integerWhole numberint, int32, int64Not a JSON type; OpenAPI narrows number
booleantrue/falsebool, booleanAvoid tri-state booleans; use an enum
arrayOrdered listList<T>, T[]items is required for a useful generated type
objectKey-value mapClass, struct, interfaceWithout properties, this becomes an untyped map
nullnullOptional/nullable wrapper3.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, which is open to formats defined by other specifications.

FormatBase typeMeaningTypical generated type
int32integer32-bit signedint, Integer
int64integer64-bit signedlong, int64
floatnumberSingle precisionfloat
doublenumberDouble precisiondouble, float64
datestringRFC 3339 full-datedate, LocalDate
date-timestringRFC 3339 date-timedatetime, time.Time
uuidstringRFC 4122 UUIDUUID type where one exists
bytestringBase64-encodedbytes, byte[]
binarystringRaw octets (3.0 file uploads)File, stream, buffer
passwordstringHint to mask in UIsstring
email, uri, hostname, ipv4, ipv6stringJSON Schema formatsstring 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.
# 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:

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 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 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.

KeywordValidation meaningGenerated result
oneOfMatches exactly one subschemaTagged union when a discriminator is present
anyOfMatches one or more subschemasUsually an untagged union or a widened type
allOfMatches every subschemaMerged/composed type, often used for inheritance
notMatches no subschemaRarely representable; usually dropped

oneOf is the one that generates well, and only when paired with a discriminator:

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 patternGenerated resultConsumer cost
type: object with no propertiesUntyped mapNo autocomplete, no compile-time checking
Unannotated timestamp stringstringEvery consumer writes their own parser
enum with no open fallbackClosed native enumNew server values break old clients
oneOf without discriminatorStructural unionAmbiguous deserialization
anyOf across dissimilar typesWidened/loose typeType safety lost entirely
Missing requiredEverything optionalNull checks on fields that always exist
No format on integersDefault widthPrecision 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 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.

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 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 in nine languages plus the interactive API reference from the same API definition. 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 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.