diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 Rick Owens
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,51 @@
+# json-spec-openapi
+
+This package provides a way to produce
+[`openapi3`](https://hackage.haskell.org/package/openapi3) documentation from a
+[`json-spec`](https://hackage.haskell.org/package/json-spec-0.1.0.0)
+specification.
+
+## Example
+
+Given this data type:
+
+```haskell
+data User = User
+  {      name :: Text
+  , lastLogin :: UTCTime
+  }
+  deriving ToSchema via (EncodingSchema User) -- <-- ToSchema instance defined here
+instance HasJsonEncodingSpec User where
+  type EncodingSpec User =
+    JsonObject
+      '[ '("name", JsonString)
+       , '("last-login", JsonDateTime)
+       ]
+  toJSONStructure user =
+    (Field @"name" (name user),
+    (Field @"last-login" (lastLogin user),
+    ()))
+```
+
+Calling `Data.Aeson.encode (Data.OpenApi3.toSchema (Proxy :: Proxy User))`
+will produce the following Schema:
+
+```json
+{
+  "additionalProperties": false,
+  "properties": {
+    "last-login": {
+      "format": "date-time",
+      "type": "string"
+    },
+    "name": {
+      "type": "string"
+    }
+  },
+  "required": [
+    "name",
+    "last-login"
+  ],
+  "type": "object"
+}
+```
diff --git a/json-spec-openapi.cabal b/json-spec-openapi.cabal
new file mode 100644
--- /dev/null
+++ b/json-spec-openapi.cabal
@@ -0,0 +1,102 @@
+cabal-version:       3.0
+name:                json-spec-openapi
+version:             0.1.0.0
+synopsis:            json-spec-openapi
+description:
+  This package provides a way to produce
+  [`openapi3`](https://hackage.haskell.org/package/openapi3) documentation from a
+  [`json-spec`](https://hackage.haskell.org/package/json-spec-0.1.0.0)
+  specification.
+
+  = Example
+
+  Given this data type:
+
+  > data User = User
+  >   {      name :: Text
+  >   , lastLogin :: UTCTime
+  >   }
+  >   deriving ToSchema via (EncodingSchema User) -- <-- ToSchema instance defined here
+  > instance HasJsonEncodingSpec User where
+  >   type EncodingSpec User =
+  >     JsonObject
+  >       '[ '("name", JsonString)
+  >        , '("last-login", JsonDateTime)
+  >        ]
+  >   toJSONStructure user =
+  >     (Field @"name" (name user),
+  >     (Field @"last-login" (lastLogin user),
+  >     ()))
+
+  Calling `Data.Aeson.encode (Data.OpenApi3.toSchema (Proxy :: Proxy User))`
+  will produce the following Schema:
+
+  > {
+  >   "additionalProperties": false,
+  >   "properties": {
+  >     "last-login": {
+  >       "format": "date-time",
+  >       "type": "string"
+  >     },
+  >     "name": {
+  >       "type": "string"
+  >     }
+  >   },
+  >   "required": [
+  >     "name",
+  >     "last-login"
+  >   ],
+  >   "type": "object"
+  > }
+homepage:            https://github.com/owensmurray/json-spec-openapi
+license:             MIT
+license-file:        LICENSE
+author:              Rick Owens
+maintainer:          rick@owensmurray.com
+copyright:           2022 Rick Owens
+category:            JSON, OpenApi
+build-type:          Simple
+extra-source-files:
+  README.md
+  LICENSE
+
+common dependencies
+  build-depends:
+    , base      >= 4.17    && < 4.18
+    , json-spec >= 0.1.0.0 && < 0.2
+    , lens      >= 5.2.2   && < 5.3
+    , openapi3  >= 3.2.3   && < 3.3
+    , text      >= 2.0.2   && < 2.1
+
+common warnings
+  ghc-options:
+    -Wmissing-deriving-strategies
+    -Wmissing-export-lists
+    -Wmissing-import-lists
+    -Wredundant-constraints
+    -Wall
+
+library
+  import: dependencies, warnings
+  exposed-modules:     
+    Data.JsonSpec.OpenApi
+  -- other-modules:       
+  -- other-extensions:    
+  hs-source-dirs:      src
+  default-language:    Haskell2010
+
+test-suite json-spec-openapi-tests
+  import: dependencies, warnings
+  main-is: test.hs
+  type: exitcode-stdio-1.0
+  hs-source-dirs: test
+  default-language: Haskell2010
+  build-depends:
+    , json-spec-openapi
+
+    , aeson      >= 2.1.2.1  && < 2.2
+    , bytestring >= 0.11.4.0 && < 0.12
+    , hspec      >= 2.11.1   && < 2.12
+    , scientific >= 0.3.7.0  && < 0.4
+    , time       >= 1.12.2   && < 1.13
+
diff --git a/src/Data/JsonSpec/OpenApi.hs b/src/Data/JsonSpec/OpenApi.hs
new file mode 100644
--- /dev/null
+++ b/src/Data/JsonSpec/OpenApi.hs
@@ -0,0 +1,240 @@
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE TypeOperators #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+{-|
+  This module provides tools for integrating the type-level JSON
+  'Specification' with the "openapi" package.
+
+  You can use 'toOpenApiSchema' as a low-level tool to transform @json-spec@
+  'Specification's into @openapi3@ 'Schema's directly, irrespective of any
+  particular business data type.
+
+  More likely you will want to use @-XDerivingVia@ along with 'EncodingSchema'
+  or 'DecodingSchema' to derive 'ToSchema' instances for your data types.
+
+  Example, given this data type:
+
+  > data User = User
+  >   {      name :: Text
+  >   , lastLogin :: UTCTime
+  >   }
+  >   deriving ToSchema via (EncodingSchema User) -- <-- ToSchema instance defined here
+  > instance HasJsonEncodingSpec User where
+  >   type EncodingSpec User =
+  >     JsonObject
+  >       '[ '("name", JsonString)
+  >        , '("last-login", JsonDateTime)
+  >        ]
+  >   toJSONStructure user =
+  >     (Field @"name" (name user),
+  >     (Field @"last-login" (lastLogin user),
+  >     ()))
+
+  Calling @'Data.Aeson.encode' ('Data.OpenApi3.toSchema' ('Proxy' :: 'Proxy' User))@
+  will produce the following Schema:
+
+  > {
+  >   "additionalProperties": false,
+  >   "properties": {
+  >     "last-login": {
+  >       "format": "date-time",
+  >       "type": "string"
+  >     },
+  >     "name": {
+  >       "type": "string"
+  >     }
+  >   },
+  >   "required": [
+  >     "name",
+  >     "last-login"
+  >   ],
+  >   "type": "object"
+  > }
+
+  If you needed more control over the content of the schema you might also
+  consider doing something like this, e.g. in the case where you would like to
+  allow additional properties:
+
+  > data User = User
+  >   {      name :: Text
+  >   , lastLogin :: UTCTime
+  >   }
+  > instance HasJsonEncodingSpec User where
+  >   type EncodingSpec User =
+  >     JsonObject
+  >       '[ '("name", JsonString)
+  >        , '("last-login", JsonDateTime)
+  >        ]
+  >   toJSONStructure user =
+  >     (Field @"name" (name user),
+  >     (Field @"last-login" (lastLogin user),
+  >     ()))
+  > instance ToSchema User where
+  >   declareNamedSchema _proxy =
+  >       pure $
+  >         NamedSchema
+  >           Nothing
+  >           (
+  >             toOpenApiSchema (EncodingSpec User)
+  >               & set
+  >                   additionalProperties
+  >                   (Just (AdditionalPropertiesAllowed True))
+  >           )
+
+-}
+module Data.JsonSpec.OpenApi (
+  toOpenApiSchema,
+  EncodingSchema(..),
+  DecodingSchema(..),
+) where
+
+
+import Control.Lens (At(at), (&), over, set)
+import Data.JsonSpec (HasJsonDecodingSpec(DecodingSpec),
+  HasJsonEncodingSpec(EncodingSpec), Specification(JsonArray, JsonBool,
+  JsonDateTime, JsonInt, JsonNum, JsonObject, JsonString))
+import Data.OpenApi (AdditionalProperties(AdditionalPropertiesAllowed),
+  HasAdditionalProperties(additionalProperties),
+  HasFormat(format), HasItems(items), HasProperties(properties),
+  HasRequired(required), HasType(type_), NamedSchema(NamedSchema),
+  OpenApiItems(OpenApiItemsObject), OpenApiType(OpenApiArray,
+  OpenApiBoolean, OpenApiInteger, OpenApiNumber, OpenApiObject,
+  OpenApiString), Referenced(Inline), ToSchema(declareNamedSchema),
+  Schema)
+import Data.String (IsString(fromString))
+import Data.Text (Text)
+import Data.Typeable (Proxy(Proxy), Typeable)
+import GHC.TypeLits (KnownSymbol, symbolVal)
+
+
+{-|
+  Convert a 'Specification' into an OpenAPI 'Schema'. The type class
+  'Internal' is an internal and opaque implementation detail and not
+  something you should have to worry about. It /should/ already have an
+  instance for every possible 'Specification' that can be constructed. If
+  it does not, then that is a bug. Please report it! :-)
+-}
+toOpenApiSchema
+  :: (Internal spec)
+  => Proxy (spec :: Specification)
+  -> Schema
+toOpenApiSchema = internal
+
+
+class Internal (spec :: Specification) where
+  {-|
+    Given a `Specification`, produce an OpenApi schema equivalent to
+    the specification. Usually you will want to use this in conjunction with
+    `HasJsonEncodingSpec` or `HasJsonDecodingSpec`.
+
+    Example:
+
+    > data MyType = ...
+    > instance HasJsonEncodingSpec MyType where
+    >   type EncodingSpec MyType = ...
+    >
+    > schema :: Schema
+    > schema = toOpenApiSchema (Proxy :: Proxy (EncodingSpec MyType))
+
+  -}
+  internal :: Proxy spec -> Schema
+instance Internal 'JsonString where
+  internal _ =
+    mempty & set type_ (Just OpenApiString)
+instance Internal 'JsonNum where
+  internal _ =
+    mempty & set type_ (Just OpenApiNumber)
+instance Internal 'JsonInt where
+  internal _ =
+    mempty & set type_ (Just OpenApiInteger)
+instance Internal ('JsonObject '[]) where
+  internal _ =
+    mempty
+      & set type_ (Just OpenApiObject)
+      & set additionalProperties (Just (AdditionalPropertiesAllowed False))
+instance (KnownSymbol key, Internal spec, Internal ('JsonObject more)) => Internal ('JsonObject ( '(key, spec) : more )) where
+  internal _ =
+    let
+      propertyName :: Text
+      propertyName = sym @key
+
+      propertySchema :: Schema
+      propertySchema = internal (Proxy @spec)
+    in
+      internal (Proxy @('JsonObject more))
+        & over properties (set (at propertyName) (Just (Inline propertySchema)))
+        & over required (propertyName:)
+instance (Internal spec) => Internal ('JsonArray spec) where
+  internal _ =
+    let
+      elementSchema :: Schema
+      elementSchema = internal (Proxy @spec)
+    in
+      mempty
+        & set type_ (Just OpenApiArray)
+        & set items (Just (OpenApiItemsObject (Inline elementSchema)))
+instance Internal 'JsonBool where
+  internal _ =
+    mempty & set type_ (Just OpenApiBoolean)
+
+instance Internal 'JsonDateTime where
+  internal _ =
+    mempty
+      & set type_ (Just OpenApiString)
+      & set format (Just "date-time")
+
+
+{-|
+  Helper for defining `ToSchema` instances based on `HasJsonEncodingSpec`
+  using @deriving via@.
+
+  Example:
+
+  > data MyType = ...
+  >   deriving ToSchema via (EncodingSchema MyType)
+  > instance HasJsonEncodingSchema MyType where
+  >   ...
+-}
+newtype EncodingSchema a =
+  EncodingSchema {unEncodingSchema :: a}
+instance (Typeable a, Internal (EncodingSpec a)) => ToSchema (EncodingSchema a) where
+  declareNamedSchema _ =
+    pure (NamedSchema Nothing (toOpenApiSchema (Proxy @(EncodingSpec a))))
+
+
+{-|
+  Helper for defining `ToSchema` instances based on `HasJsonDecodingSpec`
+  using @deriving via@.
+  
+  Example:
+
+  > data MyType = ...
+  >   deriving ToSchema via (DecodingSchema MyType)
+  > instance HasJsonDecodingSchema MyType where
+  >   ...
+-}
+newtype DecodingSchema a =
+  DecodingSchema {unDecodingSchema :: a}
+instance (Typeable a, Internal (DecodingSpec a)) => ToSchema (DecodingSchema a) where
+  declareNamedSchema _ =
+    pure (NamedSchema Nothing (toOpenApiSchema (Proxy @(DecodingSpec a))))
+
+
+{- | Shorthand for demoting type-level strings.  -}
+sym
+  :: forall a b.
+     ( IsString b
+     , KnownSymbol a
+     )
+  => b
+sym = fromString $ symbolVal (Proxy @a)
+
+
diff --git a/test/test.hs b/test/test.hs
new file mode 100644
--- /dev/null
+++ b/test/test.hs
@@ -0,0 +1,210 @@
+{-# LANGUAGE DataKinds #-}
+{-# LANGUAGE DerivingVia #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE NamedFieldPuns #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TypeApplications #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE UndecidableInstances #-}
+
+module Main (main) where
+
+import Control.Lens (At(at), (&), set)
+import Data.Aeson (FromJSON, ToJSON, encode)
+import Data.JsonSpec (Field(Field), HasJsonDecodingSpec(DecodingSpec,
+  fromJSONStructure), HasJsonEncodingSpec(EncodingSpec, toJSONStructure),
+  SpecJSON(SpecJSON), Specification(JsonArray, JsonBool, JsonDateTime,
+  JsonNum, JsonObject, JsonString))
+import Data.JsonSpec.OpenApi (EncodingSchema, toOpenApiSchema)
+import Data.OpenApi (ToSchema)
+import Data.Proxy (Proxy(Proxy))
+import Data.Text (Text)
+import Data.Time (UTCTime)
+import Test.Hspec (describe, hspec, it, shouldBe)
+import qualified Data.OpenApi as OA
+
+
+main :: IO ()
+main =
+  hspec $ do
+    describe "toOpenApiSchema" $ do
+      it "string" $
+        let
+          actual :: OA.Schema
+          actual =
+            toOpenApiSchema (Proxy @JsonString)
+
+          expected :: OA.Schema
+          expected = stringSchema
+        in
+          actual `shouldBe` expected
+
+      it "object" $
+        let
+          actual :: OA.Schema
+          actual =
+            toOpenApiSchema (Proxy @(
+              JsonObject '[
+                '("Foo", JsonString),
+                '("Bar", JsonString)
+              ]
+            ))
+
+          expected :: OA.Schema
+          expected =
+            mempty
+              & set OA.type_ (Just OA.OpenApiObject)
+              & set OA.properties (
+                  mempty
+                    & set (at "Foo") (Just (OA.Inline stringSchema))
+                    & set (at "Bar") (Just (OA.Inline stringSchema))
+                )
+              & set OA.required ["Foo", "Bar"]
+              & set
+                  OA.additionalProperties
+                  (Just (OA.AdditionalPropertiesAllowed False))
+        in
+          actual `shouldBe` expected
+
+      it "num" $
+        let
+          actual :: OA.Schema
+          actual =
+            toOpenApiSchema (Proxy @JsonNum)
+
+          expected :: OA.Schema
+          expected = numSchema
+        in
+          actual `shouldBe` expected
+
+      it "complex array" $
+        let
+          actual :: OA.Schema
+          actual =
+            toOpenApiSchema (Proxy @(
+              JsonArray (
+                JsonObject '[
+                  '("Foo", JsonString),
+                  '("Bar", JsonString)
+                ]
+              )
+            ))
+
+          expected :: OA.Schema
+          expected =
+            let
+              elementSchema :: OA.Schema
+              elementSchema =
+                mempty
+                  & set OA.type_ (Just OA.OpenApiObject)
+                  & set OA.properties (
+                      mempty
+                        & set (at "Foo") (Just (OA.Inline stringSchema))
+                        & set (at "Bar") (Just (OA.Inline stringSchema))
+                    )
+                  & set OA.required ["Foo", "Bar"]
+                  & set
+                      OA.additionalProperties
+                      (Just (OA.AdditionalPropertiesAllowed False))
+            in
+              mempty
+                & set OA.type_ (Just OA.OpenApiArray)
+                & set OA.items (Just (OA.OpenApiItemsObject (OA.Inline elementSchema)))
+
+        in
+          actual `shouldBe` expected
+
+      it "bool" $
+        let
+          actual :: OA.Schema
+          actual =
+            toOpenApiSchema (Proxy @JsonBool)
+
+          expected :: OA.Schema
+          expected = boolSchema
+        in
+          actual `shouldBe` expected
+
+      it "date-time" $
+        let
+          actual :: OA.Schema
+          actual = 
+            toOpenApiSchema (Proxy @JsonDateTime)
+
+          expected :: OA.Schema
+          expected =
+            stringSchema
+            & set OA.format (Just "date-time")
+        in
+          actual `shouldBe` expected
+    describe "EncodingSpec" $
+      it "works" $
+        let
+          actual :: OA.Schema
+          actual = OA.toSchema (Proxy @User)
+
+          expected :: OA.Schema
+          expected =
+            mempty
+              & set OA.type_ (Just OA.OpenApiObject)
+              & set OA.properties (
+                  mempty
+                    & set (at "name") (Just (OA.Inline stringSchema))
+                    & set (at "last-login") (Just (OA.Inline dateSchema))
+                )
+              & set OA.required ["name", "last-login"]
+              & set
+                  OA.additionalProperties
+                  (Just (OA.AdditionalPropertiesAllowed False))
+
+        in
+          encode actual `shouldBe` encode expected
+
+
+data User = User
+  {      name :: Text
+  , lastLogin :: UTCTime
+  }
+  deriving stock (Show, Eq)
+  deriving ToSchema via (EncodingSchema User)
+  deriving (ToJSON, FromJSON) via (SpecJSON User)
+instance HasJsonEncodingSpec User where
+  type EncodingSpec User =
+    JsonObject
+      '[ '("name", JsonString)
+       , '("last-login", JsonDateTime)
+       ]
+  toJSONStructure user =
+    (Field @"name" (name user),
+    (Field @"last-login" (lastLogin user),
+    ()))
+instance HasJsonDecodingSpec User where
+  type DecodingSpec User = EncodingSpec User
+  fromJSONStructure
+      (Field @"name" name,
+      (Field @"last-login" lastLogin,
+      ()))
+    =
+      pure User { name , lastLogin }
+
+
+stringSchema :: OA.Schema
+stringSchema = mempty & set OA.type_ (Just OA.OpenApiString)
+
+
+numSchema :: OA.Schema
+numSchema = mempty & set OA.type_ (Just OA.OpenApiNumber)
+
+
+boolSchema :: OA.Schema
+boolSchema = mempty & set OA.type_ (Just OA.OpenApiBoolean)
+
+
+dateSchema :: OA.Schema
+dateSchema =
+  mempty
+    & set OA.type_ (Just OA.OpenApiString)
+    & set OA.format (Just "date-time")
+
+
