-- Hoogle documentation, generated by Haddock -- See Hoogle, http://www.haskell.org/hoogle/ -- | Type-level JSON specification -- --

Motivation

-- -- This package provides a way to specify the shape of your JSON data at -- the type level. The particular use cases we focus on are enabling (but -- not providing in this package): -- --
    --
  1. Auto-generating documentation to ensure it is correct.
  2. --
  3. Auto-generating client code in front-end languages to ensure it is -- correct.
  4. --
-- -- There are already tools available to achieve this, but they all have -- one major drawback: they rely on generically derived Aeson instances. -- Some people strongly object to using generically derived Aeson -- instances for encoding/decoding http api data because of how brittle -- it is. It can be surprisingly easy accidentally break your API without -- noticing because you don't realize that a small change to some type -- somewhere affects the API representation. Avoiding this requires very -- strict discipline about how you organize and maintain your code. E.g. -- you will see a lot of comments like -- --
--   --| BEWARE, Changing any of the types in this file will change the API
--   -- representation!!
--   module My.API (...) where
--   
-- -- But then the types in this api might reference types in in other -- modules where it isn't as obvious that you might be changing the api -- when you make an update. -- -- I have even seen people go so far as to mandate that every type -- appearing on the API must be in some similar "API" module. This -- usually ends badly because you end up with a bunch of seemingly -- spurious (and quite tedious) translations between between "business" -- types and almost identical "API" types. -- -- The other option is to simply not use generically derived instances -- and code all or some of your ToJSON/FromJSON instances -- by hand. That (sometimes) helps solve the problem of making it a -- little more obvious when you are making a breaking api change. And it -- definitely helps with the ability to update the haskell type for some -- business purpose while keeping the encoding backwards compatible. -- -- The problem now though is that you can't take advantage of any of the -- above tooling without writing every instance by hand. Writing all the -- individual instances by hand defeat's the purpose because you are back -- to being unsure whether they are all in sync! -- -- The approach this library takes is to take a cue from servant -- and provide a way to specify the JSON encoding at the type level. You -- must manually specify the encoding, but you only have to do so once -- (at the type level). Other tools can then inspect the type using -- either type families or type classes to generate the appropriate -- artifacts or behavior. Aeson integration (provided by this package) -- works by using a type family to transform the spec into a new Haskell -- type whose structure is analogous to the specification. You are then -- required to transform your regular business value into a value of this -- "structural type" (I strongly recommend using type holes to make this -- easier). Values of the structural type will always encode into -- specification-complient JSON. -- --

Example

-- --
--   data User = User
--     { name :: Text
--     , lastLogin :: UTCTime
--     }
--     deriving stock (Show, Eq)
--     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 }
--   
@package json-spec @version 0.2.2.0 -- | This module provides a way to specify the shape of your JSON data at -- the type level. -- --

Example

-- --
--   data User = User
--     { name :: Text
--     , lastLogin :: UTCTime
--     }
--     deriving stock (Show, Eq)
--     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 }
--   
-- --

Motivation

-- -- The particular use cases we focus on are enabling (but not providing -- in this package): -- --
    --
  1. Auto-generating documentation to ensure it is correct.
  2. --
  3. Auto-generating client code in front-end languages to ensure it is -- correct.
  4. --
-- -- There are already tools available to achieve this, but they all have -- one major drawback: they rely on generically derived Aeson instances. -- Some people strongly object to using generically derived Aeson -- instances for encoding/decoding http api data because of how brittle -- it is. It can be surprisingly easy accidentally break your API without -- noticing because you don't realize that a small change to some type -- somewhere affects the API representation. Avoiding this requires very -- strict discipline about how you organize and maintain your code. E.g. -- you will see a lot of comments like -- --
--   --| BEWARE, Changing any of the types in this file will change the API
--   -- representation!!
--   module My.API (...) where
--   
-- -- But then the types in this api might reference types in in other -- modules where it isn't as obvious that you might be changing the api -- when you make an update. -- -- I have even seen people go so far as to mandate that every type -- appearing on the API must be in some similar "API" module. This -- usually ends badly because you end up with a bunch of seemingly -- spurious (and quite tedious) translations between between "business" -- types and almost identical "API" types. -- -- The other option is to simply not use generically derived instances -- and code all or some of your ToJSON/FromJSON instances -- by hand. That (sometimes) helps solve the problem of making it a -- little more obvious when you are making a breaking api change. And it -- definitely helps with the ability to update the haskell type for some -- business purpose while keeping the encoding backwards compatible. -- -- The problem now though is that you can't take advantage of any of the -- above tooling without writing every instance by hand. Writing all the -- individual instances by hand defeat's the purpose because you are back -- to being unsure whether they are all in sync! -- -- The approach this library takes is to take a cue from servant -- and provide a way to specify the JSON encoding at the type level. You -- must manually specify the encoding, but you only have to do so once -- (at the type level). Other tools can then inspect the type using -- either type families or type classes to generate the appropriate -- artifacts or behavior. Aeson integration (provided by this package) -- works by using a type family to transform the spec into a new Haskell -- type whose structure is analogous to the specification. You are then -- required to transform your regular business value into a value of this -- ''structural type'' (I strongly recommend using type holes to make -- this easier). Values of the structural type will always encode into -- specification-complient JSON. module Data.JsonSpec -- | Simple DSL for defining type level "specifications" for JSON data. -- Similar in spirit to (but not isomorphic with) JSON Schema. -- -- Intended to be used at the type level using -XDataKinds -- -- See JSONStructure for how these map into Haskell -- representations. data Specification -- | An object with the specified properties, each having its own -- specification. This does not yet support optional properties, although -- a property can be specified as "nullable" using JsonNullable JsonObject :: [(Symbol, Specification)] -> Specification -- | An arbitrary JSON string. JsonString :: Specification -- | An arbitrary (floating point) JSON number. JsonNum :: Specification -- | A JSON integer. JsonInt :: Specification -- | A JSON array of values which conform to the given spec. JsonArray :: Specification -> Specification -- | A JSON boolean value. JsonBool :: Specification -- | A value that can either be null, or else a value conforming to -- the specification. -- -- E.g.: -- --
--   type SpecWithNullableField =
--     JsonObject
--       '[ '("nullableProperty", JsonNullable JsonString)
--        ]
--   
JsonNullable :: Specification -> Specification -- | One of two different specifications. Corresponds to json-schema -- "oneOf". Useful for encoding sum types. E.g: -- --
--   data MyType
--     = Foo Text
--     | Bar Int
--     | Baz UTCTime
--   instance HasJsonEncodingSpec MyType where
--     type EncodingSpec MyType =
--       JsonEither
--         (
--           JsonObject
--             '[ '("tag", JsonTag "foo")
--              , '("content", JsonString)
--              ]
--         )
--         (
--           JsonEither
--             (
--               JsonObject
--                 '[ '("tag", JsonTag "bar")
--                  , '("content", JsonInt)
--                  ]
--             )
--             (
--               JsonObject
--                 '[ '("tag", JsonTag "baz")
--                  , '("content", JsonDateTime)
--                  ]
--             )
--         )
--   
JsonEither :: Specification -> Specification -> Specification -- | A constant string value JsonTag :: Symbol -> Specification -- | A JSON string formatted as an ISO-8601 string. In Haskell this -- corresponds to UTCTime, and in json-schema it corresponds to -- the "date-time" format. JsonDateTime :: Specification -- | A "let" expression. This is useful for giving names to types, which -- can then be used in the generated code. -- -- This is also useful to shorten repetitive type definitions. For -- example, this repetitive definition: -- --
--   type Triangle =
--     JsonObject
--       '[ '("vertex1",
--            JsonObject '[('x', JsonInt), ('y', JsonInt), ('z', JsonInt)])
--        , '("vertex2",
--            JsonObject '[('x', JsonInt), ('y', JsonInt), ('z', JsonInt)])
--        , '("vertex3",
--            JsonObject '[('x', JsonInt), ('y', JsonInt), ('z', JsonInt)])
--        ]
--   
-- -- Can be written more concisely as: -- --
--   type Triangle =
--     JsonLet '[("Vertex",
--               JsonObject '[('x', JsonInt), ('y', JsonInt), ('z', JsonInt)])
--              ]
--       (JsonObject
--         '[ '("vertex1", JsonRef "Vertex")
--          , '("vertex2", JsonRef "Vertex")
--          , '("vertex3", JsonRef "Vertex")
--          ])
--   
-- -- Another use is to define recursive types: -- --
--   type LabelledTree =
--     JsonLet '[ '("LabelledTree",
--                  JsonObject
--                    '[ '("label", JsonString)
--                     , '("children", JsonArray (JsonRef "LabelledTree"))
--                     ])
--              ]
--       (JsonRef "LabelledTree")
--   
JsonLet :: [(Symbol, Specification)] -> Specification -> Specification -- | A reference to a specification which has been defined in a surrounding -- JsonLet. JsonRef :: Symbol -> Specification -- | Types of this class can be encoded to JSON according to a type-level -- Specification. class HasJsonEncodingSpec a where { -- | The encoding specification. type EncodingSpec a :: Specification; } -- | Encode the value into the structure appropriate for the specification. toJSONStructure :: HasJsonEncodingSpec a => a -> JSONStructure (EncodingSpec a) -- | Types of this class can be JSON decoded according to a type-level -- Specification. class HasJsonDecodingSpec a where { -- | The decoding Specification. type DecodingSpec a :: Specification; } -- | Given the structural encoding of the JSON data, parse the structure -- into the final type. The reason this returns a Parser -- a instead of just a plain a is because there may still -- be some invariants of the JSON data that the Specification -- language is not able to express, and so you may need to fail parsing -- in those cases. For instance, Specification is not powerful -- enough to express "this field must contain only prime numbers". fromJSONStructure :: HasJsonDecodingSpec a => JSONStructure (DecodingSpec a) -> Parser a -- | Helper for defining ToJSON and FromJSON instances based -- on HasEncodingJsonSpec. -- -- Use with -XDerivingVia like: -- --
--   data MyObj = MyObj
--     { foo :: Int
--     , bar :: Text
--     }
--     deriving (ToJSON, FromJSON) via (SpecJSON MyObj)
--   instance HasEncodingSpec MyObj where ...
--   instance HasDecodingSpec MyObj where ...
--   
newtype SpecJSON a SpecJSON :: a -> SpecJSON a [unSpecJson] :: SpecJSON a -> a -- | Structural representation of JsonTag. (I.e. a constant string -- value.) data Tag (a :: Symbol) Tag :: Tag (a :: Symbol) -- | Structural representation of an object field. newtype Field (key :: Symbol) t Field :: t -> Field (key :: Symbol) t -- | JSONStructure spec is the Haskell type used to contain -- the JSON data that will be encoded or decoded according to the -- provided spec. -- -- Basically, we represent JSON objects as "list-like" nested tuples of -- the form: -- --
--   (Field @key1 valueType,
--   (Field @key2 valueType,
--   (Field @key3 valueType,
--   ())))
--   
-- -- Arrays, booleans, numbers, and strings are just Lists, Bools, -- Scientifics, and Texts respectively. -- -- If the user can convert their normal business logic type to/from this -- tuple type, then they get a JSON encoding to/from their type that is -- guaranteed to be compliant with the Specification type family JSONStructure (spec :: Specification) -- | This allows for recursive specifications. -- -- Since the specification is at the type level, and type level haskell -- is strict, specifying a recursive definition the "naive" way would -- cause an infinitely sized type. -- -- For example this won't work: -- --
--   data Foo = Foo [Foo]
--   instance HasJsonEncodingSpec Foo where
--     type EncodingSpec Foo = JsonArray (EncodingSpec Foo)
--     toJSONStructure = ... can't be written
--   
-- -- Using JsonLet prevents the specification type from being -- infinitely sized, but what about "structure" type which holds real -- values corresponding to the spec? The structure type has to have some -- way to reference itself or else it too would be infinitely sized. -- -- In order to "reference itself" the structure type has to go through a -- newtype somewhere along the way, and that's what this type is for. -- Whenever the structure type for your spec requires a self-reference, -- it will require you to wrap the recursed upon values in this type. -- -- For example: -- --
--   data Foo = Foo [Foo]
--   instance HasJsonEncodingSpec Foo where
--     type EncodingSpec Foo =
--       JsonLet
--         '[ '("Foo", JsonArray (JsonRef "Foo")) ]
--         (JsonRef "Foo")
--     toJSONStructure (Foo fs) = 
--       [ Rec (toJSONStructure f)
--       | f <- fs
--       ]
--   
newtype Rec (env :: [(Symbol, Type)]) (name :: Symbol) (spec :: Specification) Rec :: JStruct ('(name, Rec env name spec) ': env) spec -> Rec (env :: [(Symbol, Type)]) (name :: Symbol) (spec :: Specification) [unRec] :: Rec (env :: [(Symbol, Type)]) (name :: Symbol) (spec :: Specification) -> JStruct ('(name, Rec env name spec) ': env) spec -- | Directly decode some JSON accoring to a spec without going through any -- To/FromJSON instances. eitherDecode :: forall (spec :: Specification). StructureFromJSON (JSONStructure spec) => Proxy spec -> Value -> Either String (JSONStructure spec) -- | Analog of FromJSON, but specialized for decoding our "json -- representations", and closed to the user because the haskell -- representation scheme is fixed and not extensible by the user. -- -- We can't just use FromJSON because the types we are using to -- represent "json data" (i.e. the JSONStructure type family) -- already have ToJSON instances. Even if we were to make a -- bunch of newtypes or whatever to act as the json representation (and -- therefor also force the user to do a lot of wrapping and unwrapping), -- that still wouldn't be sufficient because someone could always write -- an overlapping (or incoherent) ToJSON instance of our -- newtype! This way we don't have to worry about any of that, and the -- types that the user must deal with when implementing -- fromJSONRepr can be simple tuples and such. class StructureFromJSON a instance (Data.JsonSpec.Decode.StructureFromJSON (Data.JsonSpec.Spec.JSONStructure (Data.JsonSpec.Decode.DecodingSpec a)), Data.JsonSpec.Decode.HasJsonDecodingSpec a) => Data.Aeson.Types.FromJSON.FromJSON (Data.JsonSpec.SpecJSON a) instance (Data.JsonSpec.Encode.StructureToJSON (Data.JsonSpec.Spec.JSONStructure (Data.JsonSpec.Encode.EncodingSpec a)), Data.JsonSpec.Encode.HasJsonEncodingSpec a) => Data.Aeson.Types.ToJSON.ToJSON (Data.JsonSpec.SpecJSON a)