5. Content Serialization / Deserealization

In WebApi, ToParam and FromParam are the typeclasses responsible for serializing and deserializing data. Serialization and deserialization for your data types are automatically take care of if they have generic instances without you having to write anything. You still have to derive them though.

Lets look at an example

data LatLng = LatLng
   { lat :: Double
   , lng :: Double
   } deriving Generic

To let WebApi automatically deserialize this type, we just need to give an empty instance declaration

instance FromParam LatLng 'QueryParam

And to serialize a type (in case you are writing a client), you can give a similar ToParam instance.

instance ToParam LatLng 'QueryParam

5.1. Nested Types

Nested types are serialized with a dot notation.

data UserData = UserData
    { age     :: Int
    , address :: Text
    , name    :: Text
    , location :: LatLng
    } deriving (Show, Eq, Generic)

Here the location field would be serialized as location.lat and location.lng

5.2. Writing Custom instances

Sometimes you may want to serialize/deserialize the data to a custom format. You can easily do this by writing a custom instance of ToParam and FromParam. Lets declare a datatype and try to write ToParam and FromParam instances for those.

data Location = Location { loc :: LatLng } deriving Generic

data LatLng = LatLng
    { lat :: Double
    , lng :: Double
    } deriving Generic

Lets say we want to deserialize query parameter loc=10,20 to Location where 10 and 20 are values of lat and lng respectively. We can write a FromParam instance for this as follows:

instance FromParam Location 'QueryParam where
    fromParam pt key trie = case lookupParam pt key trie of
        Just par -> case splitOnComma par of
            Just (lt, lg) -> LatLng <$> decodeParam lt <*> decodeParam lg
            Nothing       -> Validation $ Left [ParseErr key "Unable to cast to LatLng"]
        _ -> Validation $ Left [NotFound key]
  where
    splitOnComma :: ByteString -> (ByteString, ByteString)
    splitOnComma x =
      let (a, b) = break (== ',') x
      in if (BS.null a) || (BS.null b) then Nothing else Just (a, b)

fromParam takes a Proxy of our type (here, Location), a key (ByteString) and a Trie. WebApi uses Trie to store the parsed data while deserialization. fromParam returns a value of type Validation which is a wrapper over Either type carrying the parsed result.

We use lookupParam function for looking up the key (loc). If the key matches, it’ll return Just with the value of the key (in our case 10,20). Now we split this value into a tuple using splitOnComma and make a value of type LatLng using these.

Similarly, a ToParam instance for Location can be written as:

instance ToParam Location 'QueryParam where
  toParam pt pfx (Location (LatLng lt lg)) = [("loc", Just $ encodeParam lt <> "," <> encodeParam lg)]

Here we take a value of type Location and convert it into a key-value pair. WebApi uses this key-value pair to form the query string.

This example only included QueryParam but this can be easily extended to other param types.

5.3. Content Types

You can tell WebApi about the content-type of ApiOut/ApiErr using ContentTypes.

instance ApiContract MyApiService POST User where
    type FormParam    POST User = UserData
    type ApiOut       POST User = UserToken
    type ContentTypes POST User = '[JSON]

By default ContentTypes is set to JSON. That means you need to give ToJSON instances for the types associated with ApiOut/ApiErr while writing server side component and FromJSON instances while writing client side version.

Apart from JSON you can give other types such as HTML, PlainText etc. You can see a complete list here