RJson 0.3.2 → 0.3.3
raw patch · 3 files changed
+192/−15 lines, 3 filesPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
API changes (from Hackage documentation)
- Text.RJson: instance [overlap ok] (Data FromJsonD t, Data TranslateFieldD t) => FromJson t
+ Text.RJson: fromJson :: (FromJson a) => a -> JsonData -> Either String a
+ Text.RJson: instance [overlap ok] (Data FromJsonD t, TranslateField t) => FromJson t
- Text.RJson: genericFromJson :: (Data FromJsonD a, Data TranslateFieldD a) => a -> JsonData -> Either String a
+ Text.RJson: genericFromJson :: (Data FromJsonD a, FromJson a, TranslateField a) => a -> JsonData -> Either String a
Files
- README +175/−0
- RJson.cabal +4/−1
- Text/RJson.hs +13/−14
+ README view
@@ -0,0 +1,175 @@+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.
RJson.cabal view
@@ -1,5 +1,5 @@ Name: RJson-Version: 0.3.2+Version: 0.3.3 Cabal-Version: >= 1.2 License: BSD3 License-File: LICENSE@@ -8,8 +8,11 @@ Category: Text Synopsis: A reflective JSON serializer/parser. Stability: experimental+Extra-Source-Files: README+Build-Type: Simple Description:+ See included README for some examples. This package uses the Scrap Your Boilerplate With Class approach to generics to implement a reflective Json serializer and deserializer. Nested record types can be automatically converted to corresponding
Text/RJson.hs view
@@ -6,7 +6,6 @@ -XUndecidableInstances -XTemplateHaskell -cpp #-}- module Text.RJson (TranslateField, TranslateFieldD, translateField,@@ -25,6 +24,7 @@ objectDefaults, parseJsonString, parseJsonByteString,+ fromJson, fromJsonString, fromJsonByteString, genericFromJson,@@ -194,9 +194,7 @@ toJsonProxy :: Proxy ToJsonD toJsonProxy = error "'toJsonProxy' value should never be evaluated!" --- Again, note inclusion of translateField from TranslateField,--- and TranslateField qualification on variable 't' (in addition--- to the expected ToJson qualification).+-- Again, note inclusion of translateField from TranslateField. instance ToJson t => Sat (ToJsonD t) where dict = ToJsonD { toJsonD = toJson, excludeD = exclude,@@ -301,7 +299,7 @@ toJson = genericToJson -- Instances for tuples up to n=7 (this limit it is set by the non-existence of Typeable8).--- Tuples are converted to (hetrogenous) JSON lists.+-- Tuples are converted to (heterogenous) JSON lists. #define I(x) ToJson x, Typeable x instance (I(a), I(b)) => ToJson (a, b) where toJson (a,b) = JDArray [toJson a, toJson b]@@ -345,6 +343,7 @@ fromJsonProxy :: Proxy FromJsonD fromJsonProxy = error "'fromJsonProxy' should never be evaluated!" +-- Note inclusion of translateField from TranslateField. instance FromJson t => Sat (FromJsonD t) where dict = FromJsonD { fromJsonD = fromJson, objectDefaultsD = objectDefaults,@@ -399,7 +398,7 @@ in if isRight r1 && isRight r2 then Right $ Union (fromRight r1) (fromRight r2)- else Left "Bad fromJson conversion: error constructing subpart of union"+ else Left "Bad fromJson conversion: error constructing subpart of union (did not serialize to object)" fromJson _ _ = Left "Bad fromJson conversion: attempt to convert non-object to Union" tuperror :: Int -> Either String a@@ -484,18 +483,18 @@ -- TODO: Again, uninformative name. -- TODO: Some code duplication here.-m2 :: (Data FromJsonD a) => M.Map String JsonData -> a -> ErrorWithState String (M.Map String JsonData, [String]) a-m2 defaults dummy = do+m2 :: (Data FromJsonD a, TranslateField a) => M.Map String JsonData -> (String -> String) -> a -> ErrorWithState String (M.Map String JsonData, [String]) a+m2 defaults transFunc dummy = do (m, sl) <- lift get (case sl of [] -> throwError "Bad fromJson conversion: Not enough fields in JSON object to satisfy constructor" (f:fs) -> do lift $ put (m, fs)- let stripped = translateFieldD'' dict dummy f+ let stripped = transFunc f (case M.lookup stripped m of Nothing -> case M.lookup stripped defaults of- Nothing -> throwError $ "Bad fromJson conversion: Required field not present in JSON object: "+ Nothing -> throwError $ "Bad fromJson conversion: Required field not present in JSON object: " ++ stripped Just v -> case fromJsonD dict dummy v of Left e -> throwError e@@ -505,7 +504,7 @@ Left e -> throwError e Right x -> return x)) -genericFromJson :: (Data FromJsonD a, Data TranslateFieldD a) => a -> JsonData -> Either String a+genericFromJson :: (Data FromJsonD a, FromJson a, TranslateField a) => a -> JsonData -> Either String a genericFromJson dummy (JDArray l) = case datarep (dataTypeOf fromJsonProxy dummy) of AlgRep (c:_) ->@@ -520,7 +519,7 @@ -- Can't use fromConstrM because we need to get dummy values of the -- appropriate type for each argument of the constructor. This is unfortunate, -- since it means that we get runtime errors for records with strict fields.- fs -> evalState (runErrorT (gmapM fromJsonProxy (m2 (objectDefaultsD dict dummy)) (fromConstr fromJsonProxy c))) (m, fs)+ fs -> evalState (runErrorT (gmapM fromJsonProxy (m2 (objectDefaultsD dict dummy) (translateFieldD'' dict dummy)) (fromConstr fromJsonProxy c))) (m, fs) AlgRep _ -> Left "Bad fromJson conversion: Type with no constructors!" _ -> Left "Bad fromJson conversion: Non-algebraic datatype given to 'genericFromJson'" genericFromJson _ _ = Left "Bad fromJson conversion: Expecting JSON object or array"@@ -540,7 +539,7 @@ else Left "Constructor name not recognized in enumFromJson" enumFromJson _ _ _ = Left "Non-string given to enumFromJson" -instance (Data FromJsonD t, Data TranslateFieldD t) => FromJson t where+instance (Data FromJsonD t, TranslateField t) => FromJson t where fromJson = genericFromJson @@ -576,7 +575,7 @@ -- If we can't figure it out, guess at UTF-8. else "UTF-8" --- Converts a String of byte values of given encoding to a String of unicode code points.+-- Converts a ByteString to a String of unicode code points. toHaskellString :: EncodingName -> B.ByteString -> String toHaskellString enc source = stripBOM $ map chr (pairBytes (B.unpack bs))