openapi-hs
OpenAPI 3.1 data model
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-hsis a fork ofbiocad/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 removednullable. - Numeric
exclusiveMaximum/exclusiveMinimum, independent ofmaximum/minimum. - Tuples via
prefixItems(+items: false) instead of the removeditemsarray form. - Conditional & assertion keywords:
if/then/else,const,contains/minContains/maxContains,dependentSchemas/dependentRequired,unevaluatedProperties/unevaluatedItems, content keywords, andexamples. - JSON Schema identification keywords:
$id,$anchor,$defs,$ref,$dynamicRef,$dynamicAnchor. - Top-level 3.1 features:
webhooks,Info.summary,License.identifier, and$refonPathItem. - Schema validation that understands the new 3.1 keywords.
ToSchemaderivation to generate schemas from your Haskell types viaGHC.Generics.lensandopticsaccessors 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 anyopenapiversion outside3.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
exampleexecutable emits a complete OpenAPI 3.1 contract (withinfo, a server, top-leveltags, and a uniqueoperationIdper operation) that lints cleanly undervacuum:cabal run example > openapi.json nix run nixpkgs#vacuum-go -- lint -d openapi.jsonThe first two layers are self-referential — they confirm a document agrees with this library's own model of OpenAPI 3.1.
vacuumis 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.
- base >=4.11.1.0 && <4.23
- bytestring >=0.10.8.2 && <0.13
- containers >=0.5.11.0 && <0.9
- template-haskell >=2.13.0.0 && <2.25
- time >=1.8.0.2 && <1.16
- transformers >=0.5.5.0 && <0.7
- mtl >=2.2.2 && <2.4
- text >=1.2.3.1 && <2.2
- aeson >=2.0.1.0 && <2.3
- aeson-pretty >=0.8.7 && <0.9
- base-compat-batteries >=0.11.1 && <0.16
- cookie >=0.4.3 && <0.6
- generics-sop >=0.5.1.0 && <0.6
- hashable >=1.2.7.0 && <1.6
- http-media >=0.8.0.0 && <0.9
- insert-ordered-containers >=0.2.3 && <0.4
- lens >=4.16.1 && <5.4
- optics-core >=0.2 && <0.5
- optics-th >=0.2 && <0.5
- QuickCheck >=2.10.1 && <2.19
- scientific >=0.3.6.2 && <0.4
- unordered-containers >=0.2.9.0 && <0.3
- uuid-types >=1.0.3 && <1.1
- vector >=0.12.0.1 && <0.14
- 4.0.0