Jordan provides an abstract, inspectable way to work with JSON representations of Haskell objects. It uses a variant of finally-tagless style in order to represent JSON serialization and parsing as instructions, which can then be used to make parsers or serializers. Notably, this means that Jordan avoids intermediate maps and other data structures: Jordan serialization writes directly to Text, and Jordan deserialization parses directly to objects!
This repository consists of several, small haskell packages:
- jordan, located in
jordan/
, provides core functionality. More specifically, it provides:- An abstract API for representing JSON serialization
- An abstract API for representing JSON parsing
- A concrete, attoparsec-based JSON parser
- A concrete, custom-parsing based "reporting parser", which generates nice error reports for JSON that is syntactically valid but semantically wrong for the type
- A concrete, Builder-based JSON serializer
- jordan-openapi, which provides a way to generate OpenAPI documentation from the abstract representation of serialization or deserialization Jordan provides.
- jordan-servant, which provides API combinators for use with Servant, as well as:
- A Jordan parser which parses from a query string, letting you use any instance of
Jordan.FromJSON.Class.FromJSON
to parse parts of a query string. - A Jordan serializer which serializes to a query string, letting you use any instance of
Jordan.ToJSON.Class.ToJSON
to build part of a query string
- A Jordan parser which parses from a query string, letting you use any instance of
- jordan-servant-server, which provides server-side functionality for Jordan servant combinators. Uniquely, this package provides an error report on invalid JSON, informing the user why their JSON was not accepted by the server in a standardized way.
- jordan-servant-client, which provides client-side functionality for Jordan servant combinators. This allows you to talk to a Jordan-enhanced API from Haskell code.
- jordan-servant-openapi, which provides automatic documentation for APIs built with
jordan-servant
. Since this documentation is generated from theFromJSON
instances you defined withjordan
, it can never get out of sync with how your actual API works!
This project is highly alpha and should be considered a proof of concept.
Jordan, like the venerable aeson package, provides a way to turn your Haskell types to and from JSON. Unlike Aeson, however, Jordan has an intentionally restricted interface, which forces you to describe your parsers and serializers as a sequence of instructions. So, instead of defining a function that actually keys into some representation of a JSON object, you define things abstractly:
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
import Data.Text (Text)
import GHC.Generics
import Jordan
import Jordan.Types.JSONError
data Person = Person { firstName :: Text, lastName :: Text }
deriving (Show, Read, Eq, Ord, Generic)
instance FromJSON Person where
fromJSON = parseObject $ Person <$> parseField "firstName" <*> parseField "lastName"
instance ToJSON Person where
toJSON = serializeObject $ divide dividePerson (serializeField "firstName") (serializeField "lastName")
where
dividePerson (Person first last) = (first, last)
myPerson :: Person
myPerson = Person "Richard" "Evans"
This abstract representation allows us to "plug in" different parsers. For example, we have basic JSON parsing:
parsedAtto :: Either String Person
parsedAtto = parseViaAttoparsec "{ \"firstName\": \"Mike\", \"lastName\": \"Stoklassa\" }"
But, since the way we parse is abstract, we can also swap out other parsers. For example, here's one that uses a special "error reporting" parser, which will give you back a nice description of "what went wrong" if supplied invalid JSON:
-- A special parser that will give us an error report if something is wrong
parsedReport :: Either JSONError Person
parsedReport = parseOrReport "{ \"firstName\": \"Jack\", \"lastName\": [] }"
We can also serialize JSON, of course:
viaBuilder = toJSONViaBuilder myPerson
Since we keep serializing and parsing abstract, we can actually use them to do more things than plain old JSON conversion.
jordan-servant
, for example, provides a way to use those serializers and parsers to roundtrip to/from a query string.
jordan-openapi
uses them to generate Open API documentation.
You can even write your own serializer or parser: maybe you want to convert back and forth from some binary format, or from JSON schema.
As long as you can implement a few typeclasses, you can provide this functionality for all types that work with Jordan.
Open up an issue if you have a problem or a feature request. Open up a pull request if you have a patch.
This README is actually a literate Haskell file, so I need a main
.
Here's that:
main = do
print parsedAtto
print parsedReport
print viaBuilder