@hackage RJson0.3.4

A reflective JSON serializer/parser.

Author: Alex Drummond. Thanks to Dustin DeWeese for a patch fixing a bug in the parser.

This is some documentation on how to use the library. I wrote a blog post about an older version of the library which may contain some useful information on using syb-with-class:

http://lingnerd.blogspot.com/2007/12/pushing-haskells-type-system-to-limits.html


Not all features of the library are covered in this document yet. Unfortunately, the haddock documentation will not build at the moment because of the Template Haskell code in RJson.hs. You might want to look at Text/RJson.hs to check out the Haddock comments for the methods of ToJson and FromJson.


Suppose we have the following datatypes:

data TestRecord2 = TestRecord2 {
   _c :: Int,
   _d :: String
} deriving Show

data TestRecord1 = TestRecord1 {
   _a :: String,
   _b :: TestRecord2
} deriving Show

In order to use RJson, we first have to derive instances of Data and Typeable for these types. The following options/modules are required:

{-# OPTIONS_GHC
 -XTemplateHaskell
 -XFlexibleInstances
 -XMultiParamTypeClasses
 -XFlexibleContexts
 -XUndecidableInstances #-}
import Text.RJson
import Data.Generics.SYB.WithClass.Basics
import Data.Generics.SYB.WithClass.Derive

The following Template Haskell code can be used to derive the instances automatically:

$(derive[''TestRecord1, ''TestRecord2])

Now we can use the 'toJson' function to serialize TestRecord1 and TestRecord2 structures. For example, the expression

toJson (TestRecord1 { _a="foo", _b=TestRecord2 { _c=5, _d="bar"}})

will evaluate to the following JsonData object:

{"a":"foo","b":{"c":5,"d":"bar"}

You can just pass this object to 'show' to convert it to a string, or use the 'toJsonString' utility function. The current implementation of 'show' outputs ASCII-only strings, using "\uXXXX" escape sequences for unicode characters. Note that the initial underscores have been stripped from the field names. This is the default behavior, but we could override it by adding an instance to the TranslateField class:

instance TranslateField TestRecord1 where
    translateField _dummy x = x
instance TranslateField TestRecord2 where
    translateField _dummy x = x

Now if we call 'toJson', the underscores will not be removed:

{"_a":"foo","_b":{"_c":5,"_d":"bar"}

The 'fromJson' function is used to deserialize a JsonData object. Usually, it is easier to use 'fromJsonString', which parses a string to a JsonData object and then passes the result to 'fromJson'. The following expression will evaluate to Just the same TestRecord1 structure that we passed to 'toJson' earlier:

fromJsonString (undefined :: TestRecord1) "{\"_a\":\"foo\",\"_b\":{\"_c\":5,\"_d\":\"bar\"}}"
--> Right (TestRecord1 { _a="foo", _b=TestRecord2 { _c=5, _d="bar"}})

The first parameter of 'fromJsonString' (and 'fromJson') is a dummy value specifying the type of the record which is being deserialized. Note that the preceding instance of TranslateField is in effect here (the JSON object keys begin with underscores).

The 'fromJsonString' function assumes that the string it is passed is a true unicode string. For this reason, if you have obtained your JSON String using the standard Haskell IO libraries, you may not get the correct behavior with unicode strings (since your String will be a sequence of bytes rather than code points). It is usually better to get the raw JSON data into a ByteString and then use 'fromJsonByteString', which automatically detects and decodes unicode strings.

The JsonData type has the following definition:

data JsonData = JDString String                          |
                JDNumber Double                          |
                JDArray [JsonData]                       |
                JDBool Bool                              |
                JDObject (Data.Map.Map String JsonData)

You can implement custom serialization and deserialization behavior by adding instances to the ToJson and FromJson classes respectively. Suppose that we have the following enum type:

data Direction = Forward | Back | Left | Right deriving Show
$(derive[''Direction])

As will be explained shortly, the default serialization behavior is for the values of this enum to be converted to empty JSON lists, which is probably not what you want. In order to convert them to and from the appropriate strings, the following instance definitions can be added:

instance ToJson Direction where
    toJson North   = JDString "north"
    toJson South   = JDString "south"
    toJson East    = JDString "east"
    toJson West    = JDString "west"
instance FromJson Direction where
    fromJson _dummy (JDString "north")  = Right North
    fromJson _dummy (JDString "south")  = Right South
    fromJson _dummy (JDString "east")   = Right East
    fromJson _dummy (JDString "west")   = Right West
    fromJson _dummy _                   = Left "Deserialization error for 'Direction'"

In fact, RJson provides 'enumToJson' and 'enumFromJson' functions which automate the definition of instances of this sort. The preceding instance definitions could equivalently be written as follows:

instance ToJson Direction where
    toJson = enumToJson firstCharToLower
instance FromJson Direction where
    fromJson = enumFromJson firstCharToUpper

The first arguments to 'enumToJson' and 'enumFromJson' are (String->String) functions used for converting Haskell enum constructor names to JSON strings and vice versa. The functions 'firstCharToUpper' and 'firstCharToLower' are provided by RJson.

Default serialization behavior is as follows:

Haskell primitive types  <-->  Corresponding JSON type
Haskell records          <-->  JSON objects
Haskell tupels           <-->  Heterogenous JSON arrays
Haskell algebraic types  <-->  JSON array of arguments given to
                               constructor. First constructor
                               always used when deserializing.
                               (Not a very useful default.)

Both ToJson and FromJson have some other methods which can be used to customize serialization behavior (check the Haddock documentation). For example, you can specify default field values for JSON objects. Note that there is no default implementation of the 'toJson' or 'fromJson' methods, so if you are overriding other methods in an instance declaration of ToJson or FromJson, you can set 'toJson' and 'fromJson' to 'genericToJson' and 'genericFromJson' respectively in order to get the default behavior.

The 'Union' type can be used to implement a kind of crude inheritance for Haskell record types. The type has a single binary constructor ('Union'). Unions are serialized by serializing each of the arguments to the constructor, then merging the resulting JSON objects into a single object. If any of the arguments of the constructor does not serialize to a JSON object then a runtime error will occur. To create unions of more than two records, just use Union as an infix constructor. Type synonyms are defined for complex unions of this kind (Union3 a b c, Union4 a b c d, etc. etc.)

WARNING: Record types with strict constructors will lead to runtime errors when using 'fromJson' ('toJson' will still work fine). This is because it seems to be necessary to temporarily create records with dummy field values. If the fields are strict, these dummy values get evaluated, leading to an exception being raised.

OTHER WEIRD BUG: You cannot have a field of type X and a field whose type is a synonym of X in the same record. This leads to weird compile-time errors if you try to serialize the record. I have no idea why (but this is normally easy to work around).