Etymon.Schema.OpenApi 0.1.0-preview.9

This is a prerelease version of Etymon.Schema.OpenApi.
dotnet add package Etymon.Schema.OpenApi --version 0.1.0-preview.9
                    
NuGet\Install-Package Etymon.Schema.OpenApi -Version 0.1.0-preview.9
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Etymon.Schema.OpenApi" Version="0.1.0-preview.9" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Etymon.Schema.OpenApi" Version="0.1.0-preview.9" />
                    
Directory.Packages.props
<PackageReference Include="Etymon.Schema.OpenApi" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Etymon.Schema.OpenApi --version 0.1.0-preview.9
                    
#r "nuget: Etymon.Schema.OpenApi, 0.1.0-preview.9"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Etymon.Schema.OpenApi@0.1.0-preview.9
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Etymon.Schema.OpenApi&version=0.1.0-preview.9&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Etymon.Schema.OpenApi&version=0.1.0-preview.9&prerelease
                    
Install as a Cake Tool

Etymon.Schema.OpenApi

JSON Schema 2020-12 and OpenAPI 3.1 from an Etymon schema.

Depends on Etymon.Core and Etymon.Schema, and on no NuGet package beyond FSharp.Core.

OpenApi.toJsonSchema personSchema          // a standalone JSON Schema 2020-12 document
OpenApi.toComponents personSchema          // the components/schemas object for OpenAPI 3.1
OpenApi.toComponentsWithRoot personSchema  // both, with the root addressed at them
OpenApi.toJsonSchemaText personSchema      // the same, rendered as indented text

Assembling one document from many operations wants the pair: toComponentsWithRoot gives the named types for components/schemas and a root already addressed at #/components/schemas/…, so nothing has to re-address anything:

let listing = OpenApi.toComponentsWithRoot (Schema.list accountSchema)
// listing.Root       = { "type": "array", "items": { "$ref": "#/components/schemas/Account" } }
// listing.Components = { "Account": { … } }

Doing that lift by hand has a trap in it. The natural implementation reads $ref at the root of the standalone document and rewrites it — correct for an operation returning one object, and silently wrong for one returning a list, because an array carries its $ref under items and the root has none. The lift finds nothing and the response is described as an empty object. Nothing fails, the document is still valid OpenAPI, and a test asserting "this path is described" is satisfied by a description that says nothing.

The document is derived from the same schema value that does the encoding and decoding, so it cannot describe something different from what your code actually accepts. Nothing here reads the codec — it is a pure function of the schema's description.

What comes out

let personSchema =
    Schema.object "Person" {
        let! name = Schema.required "name" (Schema.string |> Schema.constrain Check.nonEmpty) (fun p -> p.Name)
        and! age = Schema.required "age" (Schema.int |> Schema.constrain (Check.intRange (Some 0) (Some 130))) (fun p -> p.Age)
        and! email = Schema.optional "email" Email.schema (fun p -> p.Email)
        and! address = Schema.required "address" Address.schema (fun p -> p.Address)
        return { Name = name; Age = age; Email = email; Address = address }
    }
{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$ref": "#/$defs/Person",
  "$defs": {
    "Person": {
      "type": "object",
      "properties": {
        "name": { "type": "string", "minLength": 1 },
        "age": { "type": "integer", "format": "int32", "minimum": 0, "maximum": 130 },
        "email": { "type": "string", "minLength": 3, "maxLength": 254, "format": "email" },
        "address": { "$ref": "#/$defs/Address" }
      },
      "required": ["name", "age", "address"]
    },
    "Address": { "…": "…" }
  }
}

Constraints become keywords: minLength/maxLength, minimum/maximum and their exclusive forms, pattern, format, enum, minItems/maxItems/ uniqueItems. Named schemas are written once under $defs (or components/schemas) and referenced, which is also what makes a recursive type expressible. Field descriptions, defaults and examples all carry through, and a field marked Schema.sensitive comes out writeOnly.

Unions render as oneOf, one branch per case, each pinning its tag with const. A case carrying no payload requires only the tag.

Two things it deliberately will not do

It never claims additionalProperties: false. Etymon's decoder ignores keys it does not recognise. Emitting that keyword would advertise a stricter contract than the code enforces, and a generated document that lies is worse than no document.

It never invents a keyword for a rule it cannot express. Constraint.Opaque — the escape hatch for a rule written as an arbitrary predicate — has no JSON Schema equivalent. Rather than dropping it silently, its description is appended to the schema's description, so a reader is told about a rule the document cannot enforce:

Schema.int
|> Schema.describe "A count"
|> Schema.constrain (Check.opaque "even" "must be even" (fun n -> n % 2 = 0))
{ "type": "integer", "format": "int32", "description": "A count. must be even" }

Notes

Output is deterministic — fields in declaration order, definitions in name order — so a snapshot test of a generated document is meaningful and a change to it shows up as a diff in a pull request.

Text output uses relaxed JSON escaping, so a regex like ^\d+$ reads as itself rather than as ^\\d+$. The HTML-safe escaping the default encoder applies only matters when JSON is injected into a page unescaped, which is not what happens to a schema document.

Public surface

The complete public surface of this package, every value with its full signature and its documentation, is in Surface.fsi beside this file. It is written by the build from the implementation, so it is where to learn what a function hands back without compiling anything.

Licence

MIT.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (2)

Showing the top 2 NuGet packages that depend on Etymon.Schema.OpenApi:

Package Downloads
Etymon.Api

HTTP endpoints described once: method, typed route, query parameters, request and response schemas, and typed failures. Performs no HTTP itself. Separate adapter packages interpret the same declaration as a Giraffe handler, an ASP.NET Core route, a typed client, OpenAPI paths or TypeScript declarations.

Etymon

The Etymon suite: one schema definition as the source of truth for JSON codecs, validation, OpenAPI, TypeScript, configuration and database structure. This package has no code of its own; it references the parts of the suite that cost nothing but FSharp.Core. The adapters (Etymon.Api.Giraffe, Etymon.Api.AspNetCore) and the generator package (Etymon.Invariants.FsCheck) are deliberately left out, because each carries a dependency that would then be everyone's.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.1.0-preview.9 0 9/22/2026
0.1.0-preview.8 0 9/22/2026
0.1.0-preview.7 0 9/21/2026
0.1.0-preview.6 0 9/21/2026
0.1.0-preview.5 0 9/21/2026
0.1.0-preview.4 30 9/21/2026
0.1.0-preview.3 31 9/21/2026
0.1.0-preview.2 38 9/21/2026
0.1.0-preview.1 32 9/20/2026