| Copyright | (c) Dong Han 2019 |
|---|---|
| License | BSD |
| Maintainer | winterland1989@gmail.com |
| Stability | experimental |
| Portability | non-portable |
| Safe Haskell | None |
| Language | Haskell2010 |
Z.Data.JSON
Description
Types and functions for working efficiently with JSON data, the design is quite similar to aeson or json:
- Encode to bytes can be done directly via
EncodeJSON. - Decode are split in two step, first we parse JSON doc into
Value, then convert to haskell data viaFromValue. ToValueare provided so that other doc formats can be easily supported, such asYAML.
How to use this module.
This module is intended to be used qualified, e.g.
import qualified Z.Data.JSON as JSON
import Z.Data.JSON ((.:), ToValue(..), FromValue(..), EncodeJSON(..))
The easiest way to use the library is to define target data type, deriving Generic and following instances:
FromValue, which providesfromValueto convertValueto Haskell values.ToValue, which providesToValueto convert Haskell values toValue.EncodeJSON, which providesencodeJSONto directly write Haskell value into JSON bytes.
The Generic instances convert(encode) Haskell data with following rules:
- Constructors without payloads are encoded as JSON String,
data T = A | Bare encoded as"A"or"B". - Single constructor are ingored if there're payloads,
data T = T ...,Tis ingored: - Records are encoded as JSON object.
data T = T{k1 :: .., k2 :: ..}are encoded as{"k1":...,"k2":...}. - Plain product are encoded as JSON array.
data T = T t1 t2are encoded as "[x1,x2]". - Single field plain product are encoded as it is, i.e.
data T = T tare encoded as "x" just like its payload. - Multiple constructors are convert to single key JSON object if there're payloads:
- Records are encoded as JSON object like above.
data T = A | B {k1 :: .., k2 :: ..}are encoded as{"B":{"k1":...,"k2":...}}inB .. ..case, or"A"inAcase. - Plain product are similar to above, wrappered by an outer single-key object layer marking which constructor.
These rules apply to user defined ADTs, but some built-in instances have different behaviour, namely:
Maybe aare encoded as JSONnullinNothingcase, or directly encoded to its payload inJustcase.[a]are encoded to JSON array, including[Char], i.e. there's no special treatment toString. To get JSON string, useTextorStr.NonEmpty,Vector,PrimVector,HashSet,FlatSet,FlatIntSetare also encoded to JSON array.HashMap,FlatMap,FlatIntMapare encoded to JSON object.
There're some modifying options if you providing a custom Settings, which allow you to modify field name or constructor
name, but please don't produce control characters during your modification, since we assume field labels and constructor
name won't contain them, thus we can save an extra escaping pass. To use constom Settings just write:
data T = T {fooBar :: Int, fooQux :: [Int]} deriving (Generic)
instance ToValue T where toValue = JSON.gToValue JSON.defaultSettings{ JSON.fieldFmt = JSON.snakeCase } . from
> JSON.toValue (T 0 [1,2,3])
Object [("foo_bar",Number 0.0),("bar_qux",Array [Number 1.0,Number 2.0,Number 3.0])]
Write instances manually.
You can write ToValue and FromValue instances by hand if the Generic based one doesn't suit you. Here is an example
similar to aeson's.
import qualified Z.Data.Text as T
import qualified Z.Data.Vector as V
import qualified Z.Data.Builder as B
data Person = Person { name :: T.Text , age :: Int } deriving Show
instance FromValue Person where
fromValue = JSON.withFlatMapR "Person" $ \ v -> Person
<$> v .: "name"
<*> v .: "age"
instance ToValue Person where
toValue (Person n a) = JSON.Object $ V.pack [("name", toValue n),("age", toValue a)]
instance EncodeJSON Person where
encodeJSON (Person n a) = B.curly $ do
B.quotes "name" >> B.colon >> encodeJSON n
B.comma
B.quotes "age" >> B.colon >> encodeJSON a
> toValue (Person "Joe" 12)
Object [("name",String "Joe"),("age",Number 12.0)]
> JSON.convert' @Person . JSON.Object $ V.pack [("name",JSON.String "Joe"),("age",JSON.Number 12.0)]
Right (Person {name = "Joe", age = 12})
> JSON.encodeText (Person "Joe" 12)
"{"name":"Joe","age":12}"
The Value type is different from aeson's one in that we use Vector (Text, Value) to represent JSON objects, thus
we can choose different strategies on key duplication, the lookup map type, etc. so instead of a single withObject,
we provide withHashMap, withHashMapR, withHashMap and withHashMapR which use different lookup map type, and different
key order piority. Most of time FlatMap is faster than HashMap since we only use the lookup map once, the cost of
constructing a HashMap is higher. If you want to directly working on key-values, withKeyValues provide key-values
vector access.
There're some useful tools to help write encoding code in Z.Data.JSON.Builder module, such as JSON string escaping tool, etc.
If you don't particularly care for fast encoding, you can also use toValue together with value builder, the overhead is usually very small.
Synopsis
- type DecodeError = Either ParseError ConvertError
- decode :: FromValue a => Bytes -> (Bytes, Either DecodeError a)
- decode' :: FromValue a => Bytes -> Either DecodeError a
- decodeChunks :: (FromValue a, Monad m) => m Bytes -> Bytes -> m (Bytes, Either DecodeError a)
- decodeChunks' :: (FromValue a, Monad m) => m Bytes -> Bytes -> m (Either DecodeError a)
- encodeBytes :: EncodeJSON a => a -> Bytes
- encodeText :: EncodeJSON a => a -> Text
- encodeTextBuilder :: EncodeJSON a => a -> TextBuilder ()
- data Value
- parseValue :: Bytes -> (Bytes, Either ParseError Value)
- parseValue' :: Bytes -> Either ParseError Value
- parseValueChunks :: Monad m => m Bytes -> Bytes -> m (Bytes, Either ParseError Value)
- parseValueChunks' :: Monad m => m Bytes -> Bytes -> m (Either ParseError Value)
- convert :: (a -> Converter r) -> a -> Either ConvertError r
- convert' :: FromValue a => Value -> Either ConvertError a
- newtype Converter a = Converter {
- runConverter :: forall r. ([PathElement] -> Text -> r) -> (a -> r) -> r
- fail' :: Text -> Converter a
- (<?>) :: Converter a -> PathElement -> Converter a
- prependContext :: Text -> Converter a -> Converter a
- data PathElement
- data ConvertError
- typeMismatch :: Text -> Text -> Value -> Converter a
- fromNull :: Text -> a -> Value -> Converter a
- withBool :: Text -> (Bool -> Converter a) -> Value -> Converter a
- withScientific :: Text -> (Scientific -> Converter a) -> Value -> Converter a
- withBoundedScientific :: Text -> (Scientific -> Converter a) -> Value -> Converter a
- withRealFloat :: RealFloat a => Text -> (a -> Converter r) -> Value -> Converter r
- withBoundedIntegral :: (Bounded a, Integral a) => Text -> (a -> Converter r) -> Value -> Converter r
- withText :: Text -> (Text -> Converter a) -> Value -> Converter a
- withArray :: Text -> (Vector Value -> Converter a) -> Value -> Converter a
- withKeyValues :: Text -> (Vector (Text, Value) -> Converter a) -> Value -> Converter a
- withFlatMap :: Text -> (FlatMap Text Value -> Converter a) -> Value -> Converter a
- withFlatMapR :: Text -> (FlatMap Text Value -> Converter a) -> Value -> Converter a
- withHashMap :: Text -> (HashMap Text Value -> Converter a) -> Value -> Converter a
- withHashMapR :: Text -> (HashMap Text Value -> Converter a) -> Value -> Converter a
- withEmbeddedJSON :: Text -> (Value -> Converter a) -> Value -> Converter a
- (.:) :: FromValue a => FlatMap Text Value -> Text -> Converter a
- (.:?) :: FromValue a => FlatMap Text Value -> Text -> Converter (Maybe a)
- (.:!) :: FromValue a => FlatMap Text Value -> Text -> Converter (Maybe a)
- convertField :: (Value -> Converter a) -> FlatMap Text Value -> Text -> Converter a
- convertFieldMaybe :: (Value -> Converter a) -> FlatMap Text Value -> Text -> Converter (Maybe a)
- convertFieldMaybe' :: (Value -> Converter a) -> FlatMap Text Value -> Text -> Converter (Maybe a)
- class ToValue a where
- class FromValue a where
- class EncodeJSON a where
- encodeJSON :: a -> Builder ()
- defaultSettings :: Settings
- data Settings = Settings {}
- snakeCase :: String -> Text
- trainCase :: String -> Text
- gToValue :: GToValue f => Settings -> f a -> Value
- gFromValue :: GFromValue f => Settings -> Value -> Converter (f a)
- gEncodeJSON :: GEncodeJSON f => Settings -> f a -> Builder ()
Encode & Decode
type DecodeError = Either ParseError ConvertError Source #
decode :: FromValue a => Bytes -> (Bytes, Either DecodeError a) Source #
Decode a JSON bytes, return any trailing bytes.
decode' :: FromValue a => Bytes -> Either DecodeError a Source #
Decode a JSON doc, only trailing JSON whitespace are allowed.
decodeChunks :: (FromValue a, Monad m) => m Bytes -> Bytes -> m (Bytes, Either DecodeError a) Source #
Decode JSON doc chunks, return trailing bytes.
decodeChunks' :: (FromValue a, Monad m) => m Bytes -> Bytes -> m (Either DecodeError a) Source #
Decode JSON doc chunks, consuming trailing JSON whitespaces (other trailing bytes are not allowed).
encodeBytes :: EncodeJSON a => a -> Bytes Source #
Directly encode data to JSON bytes.
encodeText :: EncodeJSON a => a -> Text Source #
Text version encodeBytes.
encodeTextBuilder :: EncodeJSON a => a -> TextBuilder () Source #
JSON Docs are guaranteed to be valid UTF-8 texts, so we provide this.
Value type
A JSON value represented as a Haskell value.
The Object's payload is a key-value vector instead of a map, which parsed
directly from JSON document. This design choice has following advantages:
- Allow different strategies handling duplicated keys.
- Allow different
Maptype to do further parsing, e.g.FlatMap - Roundtrip without touching the original key-value order.
- Save time if constructing map is not neccessary, e.g. using a linear scan to find a key if only that key is needed.
Constructors
| Object !(Vector (Text, Value)) | |
| Array !(Vector Value) | |
| String !Text | |
| Number !Scientific | |
| Bool !Bool | |
| Null |
Instances
parse into JSON Value
parseValue :: Bytes -> (Bytes, Either ParseError Value) Source #
Parse Value without consuming trailing bytes.
parseValue' :: Bytes -> Either ParseError Value Source #
Parse Value, and consume all trailing JSON white spaces, if there're
bytes left, parsing will fail.
parseValueChunks :: Monad m => m Bytes -> Bytes -> m (Bytes, Either ParseError Value) Source #
Increamental parse Value without consuming trailing bytes.
parseValueChunks' :: Monad m => m Bytes -> Bytes -> m (Either ParseError Value) Source #
Increamental parse Value and consume all trailing JSON white spaces, if there're
bytes left, parsing will fail.
Convert Value to Haskell data
convert :: (a -> Converter r) -> a -> Either ConvertError r Source #
Run a Converter with input value.
Converter for convert result from JSON Value.
This is intended to be named differently from Parser to clear confusions.
Constructors
| Converter | |
Fields
| |
(<?>) :: Converter a -> PathElement -> Converter a infixl 9 Source #
Add JSON Path context to a converter
When converting a complex structure, it helps to annotate (sub)converters with context, so that if an error occurs, you can find its location.
withFlatMapR "Person" $ \o ->
Person
<$> o .: "name" <?> Key "name"
<*> o .: "age" <?> Key "age"(Standard methods like (.:) already do this.)
With such annotations, if an error occurs, you will get a JSON Path location of that error.
prependContext :: Text -> Converter a -> Converter a Source #
Add context to a failure message, indicating the name of the structure being converted.
prependContext "MyType" (fail "[error message]") -- Error: "converting MyType failed, [error message]"
data PathElement Source #
Elements of a (JSON) Value path used to describe the location of an error.
Constructors
| Key !Text | Path element of a key into an object, "object.key". |
| Index !Int | Path element of an index into an array, "array[index]". |
| Embedded | path of a embedded (JSON) String |
Instances
data ConvertError Source #
Instances
Arguments
| :: Text | The name of the type you are trying to convert. |
| -> Text | The JSON value type you expecting to meet. |
| -> Value | The actual value encountered. |
| -> Converter a |
Produce an error message like converting XXX failed, expected XXX, encountered XXX.
withScientific :: Text -> (Scientific -> Converter a) -> Value -> Converter a Source #
applies withScientific name f valuef to the Scientific number
when value is a Number and fails using typeMismatch
otherwise.
Warning: If you are converting from a scientific to an unbounded
type such as Integer you may want to add a restriction on the
size of the exponent (see withBoundedScientific) to prevent
malicious input from filling up the memory of the target system.
Error message example
withScientific "MyType" f (String "oops") -- Error: "converting MyType failed, expected Number, but encountered String"
withBoundedScientific :: Text -> (Scientific -> Converter a) -> Value -> Converter a Source #
applies withBoundedScientific name f valuef to the Scientific number
when value is a Number with exponent less than or equal to 1024.
withRealFloat :: RealFloat a => Text -> (a -> Converter r) -> Value -> Converter r Source #
@withRealFloat try to convert floating number with following rules:
- Use
±Infinityto represent out of range numbers. - Convert
NullasNaN
withBoundedIntegral :: (Bounded a, Integral a) => Text -> (a -> Converter r) -> Value -> Converter r Source #
applies withBoundedScientific name f valuef to the Scientific number
when value is a Number and value is within minBound ~ maxBound.
withKeyValues :: Text -> (Vector (Text, Value) -> Converter a) -> Value -> Converter a Source #
Directly use Object as key-values for further converting.
withFlatMap :: Text -> (FlatMap Text Value -> Converter a) -> Value -> Converter a Source #
Take a Object as an 'FM.FlatMap T.Text Value', on key duplication prefer first one.
withFlatMapR :: Text -> (FlatMap Text Value -> Converter a) -> Value -> Converter a Source #
Take a Object as an 'FM.FlatMap T.Text Value', on key duplication prefer last one.
withHashMap :: Text -> (HashMap Text Value -> Converter a) -> Value -> Converter a Source #
Take a Object as an 'HM.HashMap T.Text Value', on key duplication prefer first one.
withHashMapR :: Text -> (HashMap Text Value -> Converter a) -> Value -> Converter a Source #
Take a Object as an 'HM.HashMap T.Text Value', on key duplication prefer last one.
Arguments
| :: Text | data type name |
| -> (Value -> Converter a) | a inner converter which will get the converted |
| -> Value | |
| -> Converter a |
Decode a nested JSON-encoded string.
(.:) :: FromValue a => FlatMap Text Value -> Text -> Converter a Source #
Retrieve the value associated with the given key of an Object.
The result is empty if the key is not present or the value cannot
be converted to the desired type.
This accessor is appropriate if the key and value must be present
in an object for it to be valid. If the key and value are
optional, use .:? instead.
(.:?) :: FromValue a => FlatMap Text Value -> Text -> Converter (Maybe a) Source #
Retrieve the value associated with the given key of an Object. The
result is Nothing if the key is not present or if its value is Null,
or empty if the value cannot be converted to the desired type.
This accessor is most useful if the key and value can be absent
from an object without affecting its validity. If the key and
value are mandatory, use .: instead.
convertFieldMaybe :: (Value -> Converter a) -> FlatMap Text Value -> Text -> Converter (Maybe a) Source #
Variant of .:? with explicit converter function.
convertFieldMaybe' :: (Value -> Converter a) -> FlatMap Text Value -> Text -> Converter (Maybe a) Source #
Variant of .:! with explicit converter function.
FromValue, ToValue & EncodeJSON
class ToValue a where Source #
Typeclass for converting to JSON Value.
Minimal complete definition
Nothing
Methods
Instances
class FromValue a where Source #
Minimal complete definition
Nothing
Methods
Instances
class EncodeJSON a where Source #
Minimal complete definition
Nothing
Methods
encodeJSON :: a -> Builder () Source #
default encodeJSON :: (Generic a, GEncodeJSON (Rep a)) => a -> Builder () Source #
Instances
Generic encode/decode Settings
There should be no control charactors in formatted texts since we don't escaping those
field names or constructor names (defaultSettings relys on Haskell's lexical property).
Otherwise encodeJSON will output illegal JSON string.
snakeCase :: String -> Text Source #
Snake casing a pascal cased constructor name or camel cased field name, words are always lower cased and separated by an underscore.
trainCase :: String -> Text Source #
Train casing a pascal cased constructor name or camel cased field name, words are always lower cased and separated by a hyphen.
gFromValue :: GFromValue f => Settings -> Value -> Converter (f a) Source #
gEncodeJSON :: GEncodeJSON f => Settings -> f a -> Builder () Source #