Easy bidirectional serialization in Haskell
Switch branches/tags
Nothing to show
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
src
test
.gitignore
.travis.yml
LICENSE
README.md
Setup.hs
codec.cabal
stack.yaml

README.md

Codec Build Status Hackage

Codec makes it simple to write composable bidirectional serializers with a consistent interface.

Just define your data type normally:

data RecordB = RecordB
  { recordBString :: String
  , recordBDouble :: Double
  } deriving (Eq, Ord, Show)

and then associate each field with a codec using the =. operator:

recordBObjCodec :: JSONCodec RecordB
recordBObjCodec = asObject "RecordB" $
  RecordB
    <$> recordBString =. field "string"
    <*> recordBDouble =. field "double"

That's it! If you want, you can now define ToJSON and FromJSON instances, or just use it directly:

instance ToJSON RecordB where
  toJSON = toJSONCodec recordBObjCodec
  toEncoding = toEncodingCodec recordBObjCodec

instance FromJSON RecordB where
  parseJSON = parseJSONCodec recordBObjCodec

Support can be added for almost any serialization library, but aeson and binary support are included.

JSON example:

data RecordA = RecordA
  { recordAInt :: Int
  , recordANestedObj :: RecordB
  , recordANestedArr :: RecordB
  , recordANestedObjs :: [ RecordB ]
  } deriving (Eq, Ord, Show)

data RecordB = RecordB
  { recordBString :: String
  , recordBDouble :: Double
  } deriving (Eq, Ord, Show)

recordACodec :: JSONCodec RecordA
recordACodec = asObject "RecordA" $
  RecordA
    <$> recordAInt =. field "int"
    <*> recordANestedObj =. field' "nestedObj" recordBObjCodec
    <*> recordANestedArr =. field' "nestedArr" recordBArrCodec
    <*> recordANestedObjs =. field' "nestedObjs" (arrayOf' id id recordBObjCodec)

recordBObjCodec :: JSONCodec RecordB
recordBObjCodec = asObject "RecordB" $
  RecordB
    <$> recordBString =. field "string"
    <*> recordBDouble =. field "double"

-- serialize to array elements
recordBArrCodec :: JSONCodec RecordB
recordBArrCodec = asArray "RecordB" $
  RecordB
    <$> recordBString =. element
    <*> recordBDouble =. element

Binary example:

data RecordA = RecordA
  { recordAInt64 :: Int64
  , recordAWord8 :: Word8
  , recordANestedB :: RecordB
  } deriving (Eq, Ord, Show)

data RecordB = RecordB
  { recordBWord16 :: Word16
  , recordBByteString64 :: BS.ByteString
  } deriving (Eq, Ord, Show)

recordACodec :: BinaryCodec RecordA
recordACodec =
  RecordA
    <$> recordAInt64 =. int64le
    <*> recordAWord8 =. word8
    <*> recordANestedB =. recordBCodec

recordBCodec :: BinaryCodec RecordB
recordBCodec =
  RecordB
    <$> recordBWord16 =. word16host
    <*> recordBByteString64 =. byteString 64

A Codec is just a combination of a deserializer r a, and a serializer c -> w a.

data CodecFor r w c a = Codec
  { codecIn :: r a
  , codecOut :: c -> w a
  }
  
type Codec r w a = CodecFor r w a a

With binary for example, r is Get and w is PutM. The reason we have an extra parameter c is so that we can associate a Codec with a particular field using the =. operator:

(=.) :: (c' -> c) -> CodecFor r w c a -> CodecFor r w c' a

Codec is an instance of Functor, Applicative, Monad and Profunctor. You can serialize in any order you like, regardless of field order in the data type:

recordBCodecFlipped :: BinaryCodec RecordB
recordBCodecFlipped = do
  bs64 <- recordBByteString64 =. byteString 64
  RecordB
    <$> recordBWord16 =. word16host
    <*> pure bs64

Contributors

=. operator and Profunctor approach thanks to Xia Li-yao