openapi-hs

A Haskell library for decoding, encoding, manipulating, and validating
OpenAPI 3.1 documents — the format that
describes HTTP APIs in JSON or YAML. OpenAPI 3.1 adopts the
JSON Schema 2020-12 dialect.
Fork notice. openapi-hs is a fork of
biocad/openapi3, which is no longer actively
maintained and supports only OpenAPI 3.0. This fork brings the library up to OpenAPI 3.1.
The Haskell module namespace is unchanged (Data.OpenApi.*), so migrating is usually just a
dependency-name swap: openapi3 → openapi-hs. The fork keeps the upstream
BSD-3-Clause license and copyright.
Highlights
- Full OpenAPI 3.1 / JSON Schema 2020-12 data model with lossless JSON round-tripping.
- Type arrays for nullability (
type: ["string", "null"]) instead of the removed nullable.
- Numeric
exclusiveMaximum / exclusiveMinimum, independent of maximum / minimum.
- Tuples via
prefixItems (+ items: false) instead of the removed items array form.
- Conditional & assertion keywords:
if/then/else, const, contains /
minContains / maxContains, dependentSchemas / dependentRequired,
unevaluatedProperties / unevaluatedItems, content keywords, and examples.
- JSON Schema identification keywords:
$id, $anchor, $defs, $ref, $dynamicRef,
$dynamicAnchor.
- Top-level 3.1 features:
webhooks, Info.summary, License.identifier, and $ref on
PathItem.
- Schema validation that understands the new 3.1 keywords.
ToSchema derivation to generate schemas from your Haskell types via GHC.Generics.
lens and optics accessors for ergonomic reads and updates.
- 3.0 → 3.1 migration helpers for documents you don't control yet.
Installation
Add openapi-hs to your project's dependencies (Cabal):
build-depends: openapi-hs
then import the umbrella module, which re-exports everything you typically need:
import Data.OpenApi
Requires GHC 9.12.4 or 9.14.1.
Quick start
Build and serialize a schema
{-# LANGUAGE OverloadedStrings #-}
import Control.Lens
import Data.Aeson (encode)
import Data.OpenApi
-- "a string, or null" — 3.1 nullability via a type array
nullableString :: Schema
nullableString = mempty
& type_ ?~ OpenApiTypeArray [OpenApiString, OpenApiNull]
& description ?~ "an optional name"
-- encode nullableString == "{\"description\":\"an optional name\",\"type\":[\"string\",\"null\"]}"
Derive a schema from a Haskell type
{-# LANGUAGE DeriveGeneric #-}
import Data.Aeson (ToJSON)
import Data.Proxy (Proxy (..))
import GHC.Generics (Generic)
import Data.OpenApi
data User = User
{ name :: String
, age :: Int
} deriving (Show, Generic)
instance ToJSON User -- needed for validation (below)
instance ToSchema User
userSchema :: Schema
userSchema = toSchema (Proxy :: Proxy User)
Decode a 3.1 document
import Data.Aeson (decode)
import Data.OpenApi (Schema)
-- decode "{\"prefixItems\":[{\"type\":\"string\"},{\"type\":\"number\"}],\"items\":false}"
-- :: Maybe Schema
Validate a value against a schema
import Data.OpenApi
import Data.OpenApi.Schema.Validation (validateToJSON)
-- Using the `User` from above (which has both ToJSON and ToSchema):
-- validateToJSON returns [] when the value conforms to its derived schema,
-- or a list of human-readable errors otherwise.
ok :: [ValidationError]
ok = validateToJSON (User "Ada" 36) -- []
For validating an arbitrary JSON Value against a specific Schema, use
validateJSON :: Definitions Schema -> Schema -> Value -> [ValidationError].
Lenses and optics
Every record field has a generated accessor in both the
lens and
optics styles. Import whichever you prefer:
import Data.OpenApi -- lens accessors (Data.OpenApi.Lens)
-- or
import Data.OpenApi.Optics -- optics labels (#type, #properties, …)
A few field lenses are suffixed with _ to avoid clashing with reserved words or Prelude
names: type_, enum_, minimum_, maximum_, default_, const_, if_, then_, else_,
contains_, id_. The corresponding optics labels keep the bare name (#type, #const, …).
Migrating from OpenAPI 3.0
The 3.1 data types deliberately cannot represent 3.0-only constructs ("Strategy A"), so a 3.0
document does not decode directly. Rewrite the parsed JSON into a 3.1 shape first, using
Data.OpenApi.Migration:
import Data.Aeson (Value, decode, encode)
import Data.OpenApi (OpenApi)
import Data.OpenApi.Migration (migrate30To31)
bring30Forward :: Value -> Maybe OpenApi
bring30Forward raw30 = decode (encode (migrate30To31 raw30))
migrate30To31 recurses into every nested schema, rewriting nullable → type arrays, boolean
exclusive bounds → numeric bounds, and tuple items arrays → prefixItems + items: false.
The single-concern helpers (migrate30NullableValue, migrate30ExclusiveBoundsValue,
migrate30ItemsArrayValue) are also exported. They are intentionally deprecated to flag that
3.0 input is transitional.
See MIGRATION_3.0_TO_3.1.md for the full breaking-changes list,
worked examples, and pitfalls.
Examples
Runnable examples live in the examples/ directory. Generated specifications can be
explored interactively in any OpenAPI 3.1 viewer or editor.
Validation
The library's own correctness is checked at three complementary levels:
-
Round-trip — the test suite encodes documents and decodes them back through
FromJSON OpenApi, which rejects any openapi version outside 3.1.0 … 3.1.1, then
compares for semantic equality.
-
Schema conformance — Data.OpenApi.Schema.Validation (validateToJSON /
validateJSON) checks that values conform to their derived 3.1 schemas, including the new
keywords.
-
Authoritative conformance — the example executable emits a complete OpenAPI 3.1
contract (with info, a server, top-level tags, and a unique operationId per operation)
that lints cleanly under vacuum:
cabal run example > openapi.json
nix run nixpkgs#vacuum-go -- lint -d openapi.json
The first two layers are self-referential — they confirm a document agrees with this
library's own model of OpenAPI 3.1. vacuum is an external, authoritative linter, so it
independently catches encoder output that is valid JSON but non-conformant OpenAPI.
Building and developing
This repository ships a Nix flake providing a pinned GHC 9.12.4 toolchain. From the repository
root:
nix develop -c cabal build all
nix develop -c cabal test all
If you have a matching cabal + GHC 9.12.x on your PATH, the same commands work without the
nix develop -c prefix. The package is Cabal-only (build-type: Simple); there is no stack.yaml.
Documentation
Full API documentation is on
Hackage. Each module's Haddocks include
worked examples.
The design and implementation strategy behind the 3.1 work is documented in
docs/OPENAPI31_MIGRATION_PLAN.md.
Contributing
Bug reports, fixes, documentation improvements, and other contributions are welcome. Please open
an issue or pull request on the GitHub issue tracker.
License
openapi-hs retains the original BSD-3-Clause license of the upstream
openapi3 project, including its copyright. See the
LICENSE file for the full text; this fork's changes are released under the same
terms.
Originally derived from work by the GetShopTV and Biocad teams.