Safe Haskell | None |
---|---|
Language | Haskell98 |
JsonGrammar allows you to express a bidirectional mapping between Haskell datatypes and JSON ASTs in one go.
- data Grammar c t1 t2
- data Context
- data h :- t :: * -> * -> * = h :- t
- pure :: (t1 -> Parser t2) -> (t2 -> Maybe t1) -> Grammar c t1 t2
- many :: Grammar c t t -> Grammar c t t
- literal :: Value -> Grammar Val (Value :- t) t
- label :: Text -> Grammar Val t1 t2 -> Grammar Val t1 t2
- object :: Grammar Obj t1 t2 -> Grammar Val (Value :- t1) t2
- property :: Text -> Grammar Val (Value :- t1) t2 -> Grammar Obj t1 t2
- array :: Grammar Arr t1 t2 -> Grammar Val (Value :- t1) t2
- element :: Grammar Val (Value :- t1) t2 -> Grammar Arr t1 t2
- coerce :: Type -> Grammar Val t1 t2 -> Grammar Val t1 t2
- fromPrism :: StackPrism a b -> Grammar c a b
- defaultValue :: Eq a => a -> Grammar c t (a :- t)
- nil :: Grammar c t ([a] :- t)
- cons :: Grammar c (a :- ([a] :- t)) ([a] :- t)
- tup2 :: Grammar c (a :- (b :- t)) ((a, b) :- t)
- class Json a where
- el :: Json a => Grammar Arr t (a :- t)
- prop :: Json a => Text -> Grammar Obj t (a :- t)
- parse :: Grammar Val (a :- ()) (b :- ()) -> a -> Parser b
- serialize :: Grammar Val (a :- ()) (b :- ()) -> b -> Maybe a
- interfaces :: [SomeGrammar Val] -> [DeclarationElement]
- data SomeGrammar c where
- SomeGrammar :: Grammar c t1 t2 -> SomeGrammar c
The Aeson example
Aeson provides this example datatype:
data Person = Person { name :: Text , age :: Int } deriving Show
With these conversion functions:
{-# LANGUAGE OverloadedStrings #-} instance FromJSON Person where parseJSON (Object v) = Person <$> v .: "name" <*> v .: "age" -- A non-Object value is of the wrong type, so fail. parseJSON _ = mzero instance ToJSON Person where toJSON (Person name age) = object ["name" .= name, "age" .= age]
From JsonGrammar's point of view, the problem with writing the conversions this way is that the same thing is written down twice: from one conversion, one can figure out what the conversion in the opposite direction should look like.
In JsonGrammar, the conversion looks like this:
{-# LANGUAGE TemplateHaskell #-} deriveStackPrismsFor ["person"] ''Person instance Json Person where grammar = fromPrism person . object (prop "name" . prop "age")
This expresses the conversion in both directions in one go. The resulting parser and serializer are each other's inverse by construction.
As a bonus, if you name your grammar, JsonGrammar will generate a TypeScript definition for you:
instance Json Person where grammar = label "Person" $ fromPrism person . object (prop "name" . prop "age")
This results in this TypeScript definition:
interface Person {age : number ;name : string ;}
Types
A Grammar
provides a bidirectional mapping between a Haskell datatype and its JSON encoding. Its first type argument specifies its context: either it's defining properties (context Obj
), array elements (context Arr
) or values (context Val
).
Category * (Grammar c) | The |
IsString (Grammar Val ((:-) Value t) t) | String literals convert to grammars that expect or produce a specific JSON string |
Monoid (Grammar c t1 t2) | The |
The context of a grammar. Most combinators ask for a grammar in a specific context as input, and produce a grammar in another context.
Elemental building blocks
pure :: (t1 -> Parser t2) -> (t2 -> Maybe t1) -> Grammar c t1 t2 Source
Creates a pure grammar that doesn't specify any JSON format but just operates on the Haskell level. Pure grammars can be used in any context.
many :: Grammar c t t -> Grammar c t t Source
Try to apply a grammar as many times as possible. The argument grammar's output is fed to itself as input until doing so again would fail. This allows you to express repetitive constructions such as array elements. many
can be used in any context.
label :: Text -> Grammar Val t1 t2 -> Grammar Val t1 t2 Source
Label a value grammar with a name. This doesn't affect the JSON conversion itself, but it generates an interface definition when converting to TypeScript interfaces
.
property :: Text -> Grammar Val (Value :- t1) t2 -> Grammar Obj t1 t2 Source
Expect or produce an object property with the specified name, and a value that can be parsed/produced by the specified grammar. This function creates a grammar in the Obj
context. You can combine multiple property
grammars using the .
operator from Category
.
Use <>
to denote choice. For example, if you are creating an object with a property called "type"
, whose value determines what other properties your object has, you can write it like this:
grammar = object (propertiesA <> propertiesB) where propertiesA = property "type" "A" . fromPrism constructorA . prop "foo" propertiesB = property "type" "B" . fromPrism constructorB . prop "bar" . prop "baz"
element :: Grammar Val (Value :- t1) t2 -> Grammar Arr t1 t2 Source
Expect or produce a JSON array element whose value matches the specified Val
grammar.
coerce :: Type -> Grammar Val t1 t2 -> Grammar Val t1 t2 Source
Mark a grammar to be of a specific TypeScript type. This doesn't affect the JSON conversion, but when generating TypeScript interfaces
a coercion causes the interface generator to stop looking at the underlying grammar and just use the specified TypeScript Type
as inferred type instead.
This is useful if you write a grammar that, for example, wraps a primitive type like string (in which case you would specify
as type). Another use is when you find the generated interface can't be described by a Predefined
StringType
Grammar
, for example because it uses a generic type parameter.
Constructing grammars
defaultValue :: Eq a => a -> Grammar c t (a :- t) Source
Create a pure
grammar that expects or produces a specific Haskell value.
Wrapping constructors
cons :: Grammar c (a :- ([a] :- t)) ([a] :- t) Source
A pure
grammar that expects or produces a cons :
.
Type-directed grammars
Using grammars
parse :: Grammar Val (a :- ()) (b :- ()) -> a -> Parser b Source
Parse a JSON value according to the specified grammar.
serialize :: Grammar Val (a :- ()) (b :- ()) -> b -> Maybe a Source
Serialize a Haskell value to a JSON value according to the specified grammar.
interfaces :: [SomeGrammar Val] -> [DeclarationElement] Source
Generate a list of TypeScript interface declarations from the specified grammars.
data SomeGrammar c where Source
Wrap a Grammar
, discarding the input/output type arguments.
SomeGrammar :: Grammar c t1 t2 -> SomeGrammar c |