openapi-hs

OpenAPI 3.1 data model

openapi-hs

Hackage License BSD-3-Clause

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: openapi3openapi-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:

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

  2. Schema conformanceData.OpenApi.Schema.Validation (validateToJSON / validateJSON) checks that values conform to their derived 3.1 schemas, including the new keywords.

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