diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,5 @@
+# Revision history for jordan-openapi
+
+## 0.1.0.0 -- YYYY-mm-dd
+
+* First version. Released on an unsuspecting world.
diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,20 @@
+Copyright (c) 2021 Anthony Super
+
+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/jordan-openapi.cabal b/jordan-openapi.cabal
new file mode 100644
--- /dev/null
+++ b/jordan-openapi.cabal
@@ -0,0 +1,60 @@
+cabal-version:      3.0
+name:               jordan-openapi
+version:            0.1.0.0
+synopsis:           OpenAPI Definitions for Jordan, Automatically
+
+-- A longer description of the package.
+-- description:
+homepage:
+
+-- A URL where users can report bugs.
+-- bug-reports:
+license:            MIT
+license-file:       LICENSE
+author:             Anthony Super
+maintainer:         anthony@noided.media
+
+-- A copyright notice.
+-- copyright:
+category:           Network
+extra-source-files: CHANGELOG.md
+
+common build-deps
+  build-depends:
+      aeson  >= 1.5.6 && <1.6
+    , bytestring >= 0.10.12 && <0.11
+    , containers >= 0.6.2 && <0.7
+    , contravariant >= 1.5.5 && <1.6
+    , insert-ordered-containers >= 0.2.5 && <0.3
+    , jordan >= 0.1.0.0 && <0.2
+    , openapi3 >= 3.1.0 && <3.2
+    , optics-core >= 0.4 && <0.5
+    , text >= 1.2.4 && <1.3
+
+library
+    import: build-deps
+    exposed-modules:
+        Jordan.OpenAPI.Internal
+      , Jordan.OpenAPI
+
+    -- Modules included in this library but not exported.
+    -- other-modules:
+
+    -- LANGUAGE extensions used by modules in this package.
+    -- other-extensions:
+    build-depends:    base ^>=4.14.1.0
+    hs-source-dirs:   lib
+    default-language: Haskell2010
+
+test-suite jordan-openapi-test
+    import: build-deps
+    build-depends: jordan-openapi
+    default-language: Haskell2010
+    type:             exitcode-stdio-1.0
+    hs-source-dirs:   test
+    main-is:          JordanOpenApiSpec.hs 
+    other-modules:
+        Jordan.OpenAPI.SpecDefs
+    build-depends:
+        base ^>=4.14.1.0
+      , hspec
diff --git a/lib/Jordan/OpenAPI.hs b/lib/Jordan/OpenAPI.hs
new file mode 100644
--- /dev/null
+++ b/lib/Jordan/OpenAPI.hs
@@ -0,0 +1,41 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+module Jordan.OpenAPI
+    ( getFromNamed
+    , getToNamed
+    , JordanFromJSONSchema (..)
+    ) where
+
+import Data.Functor.Contravariant (contramap)
+import Data.OpenApi.Schema (ToSchema(..))
+import Data.Proxy (Proxy(..))
+import Data.Typeable (Typeable(..))
+import Jordan.FromJSON.Class (FromJSON(..))
+import Jordan.OpenAPI.Internal (getFromNamed, getToNamed)
+import Jordan.ToJSON.Class (ToJSON(..))
+
+-- | Newtype for use with DerivingVia.
+--
+-- Allows deriving 'Data.OpenApi.Schema.ToSchema' via DerivingVia, using the Jordan
+-- defintion of 'Jordan.ToJSON.Class.ToJSON'.
+newtype JordanFromJSONSchema a
+  = JordanFromJSONSchema { getJordanFromJSONSchema :: a }
+
+instance (FromJSON a) => FromJSON (JordanFromJSONSchema a) where
+  fromJSON = JordanFromJSONSchema <$> fromJSON
+
+instance (Typeable a, FromJSON a) => ToSchema (JordanFromJSONSchema a) where
+  declareNamedSchema (Proxy :: Proxy (JordanFromJSONSchema a)) = getFromNamed (Proxy :: Proxy a)
+
+-- | Newtype for use with DerivingVia.
+--
+-- Allows deriving 'Data.OpenApi.Schema.ToSchema' via DerivingVia, using the Jordan
+-- defintion of 'Jordan.ToJSON.Class.ToJSON'.
+newtype JordanToJSONSchema a
+  = JordanToJSONSchema { getJordanToJSONSchema :: a }
+
+instance (ToJSON a) => ToJSON (JordanToJSONSchema a) where
+  toJSON = contramap getJordanToJSONSchema toJSON
+
+instance (Typeable a, ToJSON a) => ToSchema (JordanToJSONSchema a) where
+  declareNamedSchema (Proxy :: Proxy (JordanToJSONSchema a)) = getToNamed (Proxy :: Proxy a)
diff --git a/lib/Jordan/OpenAPI/Internal.hs b/lib/Jordan/OpenAPI/Internal.hs
new file mode 100644
--- /dev/null
+++ b/lib/Jordan/OpenAPI/Internal.hs
@@ -0,0 +1,231 @@
+{-# LANGUAGE DerivingVia #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE InstanceSigs #-}
+{-# LANGUAGE LambdaCase #-}
+{-# LANGUAGE OverloadedLabels #-}
+{-# LANGUAGE OverloadedLists #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE TupleSections #-}
+module Jordan.OpenAPI.Internal
+    where
+
+import Control.Applicative (Alternative(..))
+import Control.Monad (unless)
+import qualified Data.Aeson.Types as Aeson
+import Data.Functor (void)
+import Data.Functor.Const
+import Data.Functor.Contravariant
+import Data.Functor.Contravariant.Divisible
+import qualified Data.HashMap.Strict.InsOrd as InsOrd
+import Data.Maybe (fromMaybe)
+import Data.OpenApi.Declare
+import Data.OpenApi.Internal
+import Data.OpenApi.Internal.Schema (rename, unname, unnamed)
+import Data.OpenApi.Optics
+import Data.Proxy (Proxy(..))
+import qualified Data.Text as T
+import Jordan.FromJSON.Class
+import Jordan.ToJSON.Class
+import Optics.At.Core
+import Optics.Operators
+import Optics.Optic ((%))
+
+newtype ConstDeclare env r a
+  = ConstDeclare { runConstDeclare :: Declare env r }
+
+instance Functor (ConstDeclare env r) where
+  fmap _ (ConstDeclare d) = ConstDeclare d
+
+instance (Monoid r, Monoid env) => Applicative (ConstDeclare env r) where
+  pure _ = ConstDeclare $ pure mempty
+  (ConstDeclare f) <*> (ConstDeclare a) = ConstDeclare $ do
+    f' <- f
+    a' <- a
+    pure $ f' <> a'
+
+instance Contravariant (ConstDeclare env r) where
+  contramap _ (ConstDeclare d) = ConstDeclare d
+
+instance (Monoid r, Monoid env) => Divisible (ConstDeclare env r) where
+  divide _ (ConstDeclare l) (ConstDeclare r) = ConstDeclare $ do
+    l' <- l
+    r' <- r
+    pure $ l' <> r'
+  conquer = ConstDeclare $ pure mempty
+
+newtype ObjectSchema a
+  = ObjectSchema
+  { getObjectSchema
+    :: Declare (Definitions Schema) Schema
+  } deriving (Functor, Applicative, Contravariant, Divisible) via (ConstDeclare (Definitions Schema) Schema)
+
+instance JSONObjectParser ObjectSchema where
+  parseFieldWith t p = ObjectSchema $ do
+    r <- getRefDef (getJSONSchema p)
+    pure $ (#_schemaProperties % at t ?~ r) mempty
+  parseField
+    :: forall a. FromJSON a
+    => T.Text
+    -> ObjectSchema a
+  parseField label = ObjectSchema $ do
+    ref <- getJSONRef (Proxy :: Proxy a)
+    pure $ (#_schemaProperties % at label ?~ ref) mempty
+
+instance JSONObjectSerializer ObjectSchema where
+  writeField f w = ObjectSchema $ do
+    r <- getRefDef (getJSONSchema w)
+    pure $ (#_schemaProperties % at f ?~ r) mempty
+
+newtype TupleSchema a
+  = TupleSchema
+    { getTupleSchema
+        :: Declare (Definitions Schema) [Referenced Schema]
+    } deriving (Functor, Applicative, Contravariant, Divisible) via (ConstDeclare (Definitions Schema) [Referenced Schema])
+
+instance JSONTupleParser TupleSchema where
+  consumeItemWith p = TupleSchema $ do
+    r <- getRefDef $ getJSONSchema p
+    pure [r]
+
+newtype JSONSchema a
+  = JSONSchema
+  { getJSONSchema :: Declare (Definitions Schema) NamedSchema
+  } deriving (Functor, Contravariant) via (ConstDeclare (Definitions Schema) NamedSchema)
+
+instance Semigroup (JSONSchema a) where
+  a <> b = JSONSchema . fmap unnamed $
+    combineSchemas <$> getRefDef (getJSONSchema a) <*> getRefDef (getJSONSchema b)
+
+instance Selectable JSONSchema where
+  giveUp = mempty
+  select _ (JSONSchema lhs) (JSONSchema rhs) = JSONSchema lhs <> JSONSchema rhs
+
+combineSchemas :: Referenced Schema -> Referenced Schema -> Schema
+combineSchemas = curry $ \case
+  (Inline a, Inline b) ->
+    (#_schemaOneOf ?~ fromMaybe [Inline a] (_schemaOneOf a) <> fromMaybe [Inline b] (_schemaOneOf b)) mempty
+  (Inline a, Ref b) ->
+    (#_schemaOneOf ?~ fromMaybe [Inline a] (_schemaOneOf a) <> [Ref b]) mempty
+  (Ref a, Inline b) ->
+    (#_schemaOneOf ?~ [Ref a] <> fromMaybe [Inline b] (_schemaOneOf b)) mempty
+  (Ref a, Ref b) -> (#_schemaOneOf ?~ [Ref a, Ref b]) mempty
+
+-- | Empty instance: must be both a boolean and a text value, which is not possible (obviously!)
+instance Monoid (JSONSchema a) where
+  mempty = JSONSchema $ do
+    t <- getJSONSchema parseText
+    b <- getJSONSchema parseBool
+    pure $ unnamed $ (#_schemaAllOf ?~ [Inline $ _namedSchemaSchema t, Inline $ _namedSchemaSchema b]) mempty
+
+getJSONRef
+  :: forall a. FromJSON a
+  => Proxy a
+  -> Declare (Definitions Schema) (Referenced Schema)
+getJSONRef = getRefDef . getFromNamed
+
+getRefDef
+  :: Declare (Definitions Schema) NamedSchema
+  -> Declare (Definitions Schema) (Referenced Schema)
+getRefDef decl = case undeclare decl of
+    NamedSchema (Just name) schema -> do
+      known <- looks (InsOrd.member name)
+      unless known $ do
+        declare [(name, schema)]
+        void decl
+      return $ Ref (Reference name)
+    _ -> Inline . _namedSchemaSchema <$> decl
+
+onlyUnnamed :: Schema -> JSONSchema a
+onlyUnnamed a = JSONSchema $ pure $ NamedSchema Nothing a
+
+instance JSONTupleSerializer TupleSchema where
+  writeItem s = TupleSchema $ do
+    r <- getRefDef $ getJSONSchema s
+    pure [r]
+
+instance JSONParser JSONSchema where
+  parseObject n f = JSONSchema $ do
+    d <- getObjectSchema f
+    pure $ NamedSchema (Just n) d
+  parseDictionary inner = JSONSchema $ do
+    r <- getRefDef (getJSONSchema inner)
+    pure $ unnamed $
+      ( (#_schemaType ?~ OpenApiObject)
+      . (#_schemaAdditionalProperties ?~ AdditionalPropertiesSchema r)
+      ) mempty
+  parseTuple parser = JSONSchema $ do
+    items <- getTupleSchema parser
+    pure $
+      ( unnamed
+      . (#_schemaType ?~ OpenApiArray)
+      . (#_schemaItems ?~ OpenApiItemsArray items)
+      ) mempty
+  parseArrayWith p = JSONSchema $ do
+    itemRef <- getRefDef (getJSONSchema p)
+    pure $
+      ( unnamed
+      . (#_schemaType ?~ OpenApiArray)
+      . (#_schemaItems ?~ OpenApiItemsObject itemRef)
+      ) mempty
+  parseArray
+    :: forall a. (FromJSON a)
+    => JSONSchema [a]
+  parseArray = JSONSchema $ do
+    itemRef <- getRefDef $ getFromNamed (Proxy :: Proxy a)
+    pure $
+      ( unnamed
+      . (#_schemaType ?~ OpenApiArray)
+      . (#_schemaItems ?~ OpenApiItemsObject itemRef)
+      ) mempty
+  parseNumber = onlyUnnamed $ (#_schemaType ?~ OpenApiNumber) mempty
+  parseTextConstant t
+    = void
+    . onlyUnnamed
+    . (#_schemaType ?~ OpenApiString)
+    . (#_schemaEnum ?~ [Aeson.String t])
+    $ mempty
+  parseNull = onlyUnnamed $ (#_schemaType ?~ OpenApiNull) mempty
+  parseText = onlyUnnamed $  (#_schemaType ?~ OpenApiString) mempty
+  parseBool = onlyUnnamed $ (#_schemaType ?~ OpenApiBoolean) mempty
+  validateJSON (JSONSchema d) = JSONSchema d
+
+instance JSONSerializer JSONSchema where
+  serializeObject t f = JSONSchema $ do
+    d <- getObjectSchema f
+    pure $ NamedSchema (Just t) d
+  serializeTuple t = JSONSchema $ do
+    items <- getTupleSchema t
+    pure $
+      ( unnamed
+      . (#_schemaType ?~ OpenApiArray)
+      . (#_schemaItems ?~ OpenApiItemsArray items)
+      ) mempty
+  serializeArray :: forall a. (ToJSON a) => JSONSchema [a]
+  serializeArray = JSONSchema $ do
+    itemRef <- getRefDef $ getToNamed (Proxy :: Proxy a)
+    pure $
+      ( unnamed
+      . (#_schemaType ?~ OpenApiArray)
+      . (#_schemaItems ?~ OpenApiItemsObject itemRef)
+      ) mempty
+  serializeText = parseText
+  serializeBool = parseBool
+  serializeNumber = parseNumber
+  serializeDictionary ser = JSONSchema $ do
+    r <- getRefDef (getJSONSchema ser)
+    pure $ unnamed $
+      ( (#_schemaType ?~ OpenApiObject)
+      . (#_schemaAdditionalProperties ?~ AdditionalPropertiesSchema r)
+      ) mempty
+  serializeNull =
+    case parseNull of
+      JSONSchema a -> JSONSchema a
+  serializeTextConstant t = let (JSONSchema a) = parseTextConstant t in JSONSchema a
+
+-- | Get documentation for a type that implements FromJSON
+getFromNamed :: forall a. (FromJSON a) => Proxy a -> Declare (Definitions Schema) NamedSchema
+getFromNamed p = getJSONSchema (fromJSON :: JSONSchema a)
+
+-- | Get documentation for a type that implements ToJSON
+getToNamed :: forall a. (ToJSON a) => Proxy a -> Declare (Definitions Schema) NamedSchema
+getToNamed p = getJSONSchema (toJSON :: JSONSchema a)
diff --git a/test/Jordan/OpenAPI/SpecDefs.hs b/test/Jordan/OpenAPI/SpecDefs.hs
new file mode 100644
--- /dev/null
+++ b/test/Jordan/OpenAPI/SpecDefs.hs
@@ -0,0 +1,51 @@
+{-# LANGUAGE DeriveAnyClass #-}
+{-# LANGUAGE DeriveGeneric #-}
+{-# LANGUAGE DerivingStrategies #-}
+{-# LANGUAGE OverloadedStrings #-}
+module Jordan.OpenAPI.SpecDefs
+    where
+
+import qualified Data.Map as Map
+import qualified Data.Text as T
+import GHC.Generics (Generic)
+import Jordan (FromJSON(..), ToJSON(..))
+
+data Person
+  = Person
+  { firstName :: T.Text
+  , lastName :: T.Text
+  } deriving (Show, Generic)
+  deriving anyclass (ToJSON, FromJSON)
+
+
+data Managership
+  = Managership
+  { manager :: Person
+  , underlings :: [Managership]
+  , feelings :: Map.Map T.Text T.Text
+  }
+  deriving (Show, Generic)
+  deriving anyclass (ToJSON, FromJSON)
+
+data Rank
+  = RankNested { top :: T.Text, nest :: Rank }
+  | RankSplit { left :: Rank, right :: Rank }
+  | RankBottom { label :: T.Text }
+  deriving (Show, Generic)
+  deriving anyclass (ToJSON, FromJSON)
+
+data FileEntry
+  = FileEntry
+  { name :: T.Text
+  , contents :: Either File Directory
+  }
+  deriving (Show, Eq, Ord, Generic)
+  deriving anyclass (ToJSON, FromJSON)
+
+newtype File = File { getFile :: T.Text }
+  deriving (Show, Eq, Ord, Generic)
+  deriving anyclass (ToJSON, FromJSON)
+
+newtype Directory = Directory { getEntries :: [FileEntry] }
+  deriving (Show, Eq, Ord, Generic)
+  deriving anyclass (ToJSON, FromJSON)
diff --git a/test/JordanOpenApiSpec.hs b/test/JordanOpenApiSpec.hs
new file mode 100644
--- /dev/null
+++ b/test/JordanOpenApiSpec.hs
@@ -0,0 +1,68 @@
+{-# LANGUAGE OverloadedLabels #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE TypeApplications #-}
+module Main
+    where
+
+import qualified Data.HashMap.Strict.InsOrd as I
+import Data.OpenApi.Declare (Declare, evalDeclare, execDeclare)
+import Data.OpenApi.Internal
+import Data.Proxy (Proxy(..))
+import Data.Text (Text)
+import Jordan (FromJSON(..), ToJSON(..))
+import Jordan.OpenAPI (getFromNamed, getToNamed)
+import Jordan.OpenAPI.SpecDefs
+import Optics.Operators ((^.))
+import Test.Hspec (Expectation, SpecWith, before, describe, hspec, it, shouldBe, shouldContain)
+
+main = hspec $ do
+  fileScenarios
+  entryScenarios
+  recursiveScenarios
+  topLevelScenarios
+
+shouldMakeNamedTo
+  :: (ToJSON a)
+  => Proxy a
+  -> Text
+  -> Expectation
+shouldMakeNamedTo p@Proxy name =
+  (evalDeclare (getToNamed p) mempty ^. #_namedSchemaName) `shouldBe` Just name
+
+shouldMakeUnnamed
+  :: (ToJSON a)
+  => Proxy a
+  -> Expectation
+shouldMakeUnnamed p@Proxy =
+  (evalDeclare (getToNamed p) mempty ^. #_namedSchemaName) `shouldBe` Nothing
+
+shouldDeclareTo
+  :: (ToJSON a)
+  => Proxy a
+  -> Text
+  -> Expectation
+shouldDeclareTo p@Proxy name =
+  I.keys (execDeclare (getToNamed p) mempty) `shouldContain` [name]
+
+fileScenarios = describe "basic scenarios with file-like types" $ do
+  it "works with file" $ do
+    (Proxy @File) `shouldMakeNamedTo` "Jordan.OpenAPI.SpecDefs.File.File.Output"
+  it "works with directory" $ do
+    (Proxy @Directory) `shouldMakeNamedTo` "Jordan.OpenAPI.SpecDefs.Directory.Directory.Output"
+
+entryScenarios = describe "bsaic scenarios with a directory-entry-like type" $ do
+  it "includes definition for a file" $ do
+    (Proxy @FileEntry) `shouldDeclareTo` "Jordan.OpenAPI.SpecDefs.File.File.Output"
+  it "includes definition for a directory" $ do
+    (Proxy @FileEntry) `shouldDeclareTo` "Jordan.OpenAPI.SpecDefs.Directory.Directory.Output"
+
+recursiveScenarios = describe "scenarios with a recursive data type" $ do
+  it "contains the top-level type" $ do
+    (Proxy @Managership) `shouldDeclareTo` "Jordan.OpenAPI.SpecDefs.Managership.Managership.Output"
+  it "contains a nested type" $ do
+    (Proxy @Managership) `shouldDeclareTo` "Jordan.OpenAPI.SpecDefs.Person.Person.Output"
+
+topLevelScenarios = describe "scenarios with some top-level sum type" $ do
+  it "is not named" $ do
+    shouldMakeUnnamed (Proxy @Rank)
+
