This module provides JSON support for Frege.
JSON support comprises 4 levels:
The users of this library can get full JSON support by concentrating on the third point, which essentially means that they have to write FromJSON and ToJSON instances for their data types. It follows a short tutorial on how to do this.
The following suggestions will foster a compact and easy to decode String representation of Frege data.
(There is currently no support for automatically derived instances yet, but when it gets implemented it shall comply with this suggestions.)
{ "Just": 42 } {"Nothing":null}
{ "Data": [field1,field2,...]}.
{ "case":"Person", "name":"Smith", "age":42}
This will be done in two steps:
The conversion should be done by implementing type class ToJSON for your data. There is just one class operation ToJSON.toJSON to implement.
Because standard Frege types, lists, arrays and all tuple types are already instances of ToJSON, this is easy to accomplish, as will be demonstrated below.
Given the following definitions:
data Engine = Electric Double --- just performance in kW | Combustion { fuel :: [Fuel], displacement :: Double, cyls :: Int } data Fuel = DIESEL | GASOLINE | NATURALGAS | LPG data Vehicle = Vehicle { wheels :: Int, engine :: Maybe Engine }
we could make the following instances:
derive Show Fuel -- we need this in the ToJSON instance
Use the Value.String constructor to create a JSON string value from an ordinary String.
instance ToJSON Fuel where toJSON = String . show
The instance for Engine makes JSON structs for the Engine constructors by associating the constructor name with the values under the constructor. There is a function struct, designed to create such structures that have just a single association, which is - according to our encoding suggestions - a most frequent case.
instance ToJSON Engine where toJSON (Electric d) = struct "Electric" d toJSON Combustion{fuel, displacement, cyls} = struct "Combustion" (fuel, displacement, cyls)
The Vehicle instance demonstrates a more complex JSON struct, where the record field names are associated with their values. The Value.Struct constructor takes a list of associations, created with assoc. The constructor name is redundant here, as Vehicle is a product type, so it is not included.
If we have Maybe values in the struct that are actually Maybe.Nothing, we can as well omit their associations and thus reduce the size of the generated JSON. However, the corresponding FromJSON.fromJSON function will then have to interpret a missing association as Maybe.Nothing - this is exactly the purpose of the optional function.
instance ToJSON Vehicle where {-- For illustration, we use a struct with record fields The first clause is not strictly necessary, but helps to reduce the generated json size in the case that we have no engine, by just not producing an association for "engine". This assumes that the corresponding 'fromJSON' function takes care of that, preferably by extracting the "engine" field with 'optional'. -} toJSON Vehicle{wheels, engine=Nothing} = Struct [ assoc "wheels" wheels ] -- just leave out engine toJSON Vehicle{wheels, engine} = Struct [ assoc "wheels" wheels, -- uses the short form for Maybe assoc "engine" (maybeToJSON engine), ]
We can now construct some vehicles and print a JSON document. The translation of the internal to the external (i.e. String) form is done by the Show instance for JSON Values.
bicycle = Vehicle { wheels = 2, engine = Nothing } ebike = Vehicle { wheels = 2, engine = Just (Electric 0.9) } car = Vehicle { wheels = 4, engine = Just Combustion { fuel = [LPG, GASOLINE], displacement = 2.0, cyls = 4 }} vehicles = [car, bicycle, ebike] main = println (toJSON vehicles)
The output looks like:
[{ "wheels" : 4, "engine" : {"Combustion" : [["LPG", "GASOLINE"], 2.0, 4]} }, {"wheels" : 2, "engine" : null}, {"wheels" : 2, "engine" : {"Electric" : 0.9}}]
There are 2 ways one could get confronted with JSON data:
The type class FromJSON has two operations to deal with those scenarios: FromJSON.parseJSON and FromJSON.fromJSON. But only the latter one needs to be implemented, the default implementation of FromJSON.parseJSON will parse a String to a generic JSON Value and pass this to FromJSON.fromJSON.
While it may cost a bit of memory to first create the JSON Value, this two-step approach has a number of advantages. Not the least one being clear separation of lexical and syntactical issues from semantical ones. But it is also the case that writing FromJSON.fromJSON functions is quite easy, if not boring. The type checker, the support for basic types, lists and tuples and the internal plumbing make it hard to write a wrong implementation. Whereas writing a parser is comparatively hard and error prone.
Reconstruction of values from JSON data can fail, therefore the FromJSON.fromJSON function delivers its result in a failure monad (MonadFail - two well known failure monads are Maybe and Either String). FromJSON.fromJSON is therefore overloaded twice in the return type: one for the actual data type and then the monad.
If one has already a ToJSON instance, it should go without saying that the implementations should satisfy the following law:
decodeEncoded x = fromJSON (toJSON x) == Just x
Here are, in continuation of our example from above, the FromJSON instances:
instance FromJSON Fuel where fromJSON (String "DIESEL") = return DIESEL fromJSON (String "GASOLINE") = return GASOLINE fromJSON (String "NATURALGAS") = return NATURALGAS fromJSON (String "LPG") = return LPG fromJSON s = fail ("cannot decode fuel from " ++ show s)
Note the use of MonadFail.fail and Applicative.pure which is needed because we produce our value in a failure monad like Maybe or Either String. In fact, every correct implementation of FromJSON.fromJSON will have at least two equations (or, equivalently, case alternatives) where at least one does MonadFail.fail and at least one is Applicative.pureing an actual value.
import Data.List(lookup) -- for looking up associations instance FromJSON Engine where fromJSON (Struct as) | Just n <- lookup "Electric" as = Electric <$> fromJSON n | Just fdc <- lookup "Combustion" as = do (fuel, displacement, cyls) <- fromJSON fdc return Combustion{fuel, displacement, cyls} fromJSON x = fail ("invalid engine: " ++ show x)
We need to look up the keys to find out which variant we have. We then just decode the associated values and recreate our value. Failures for sub-components are propagated upwards automatically, thanks to the monadic approach. For example, when we try to decode an engine from
{ "Combustion" : [ ["Water"], 2.7, 3]}
we will get the error
cannot decode fuel from "Water"
Because the fuel cannot get decoded, the decoding of the 3-tuple fails, and our combustion engine will not be constructed.
Note how the existence of generic FromJSON instances for primitives, tuples and lists makes the decoding a no brainer.
Finally we can decode vehicles:
instance FromJSON Vehicle where fromJSON (Struct as) = do wheels ← field "wheels" as engine ← optional "engine" as pure Vehicle{wheels, engine} fromJSON garbage = fail ("couldn't decode Vehicle from: " ++ show garbage)
The field function extracts a field from an association list, or fails with an appropriate message. The "engine" field has type (Maybe Engine) and could be extracted with the field function as well, but a missing "engine" association would then mean failure. With optional, a missing "engine" association means that "engine" is Maybe.Nothing.
Observe that this instance allows the following:
Such a forgiving and fault-tolerant implementation is oftentimes in order. However, if one needs it stricter, one can pattern match the association list directly:
fromJSON (Struct [("wheels", jw), ("engine", je)]) = do wheels <- fromJSON jw engine <- fromJSON je return Vehicle{wheels, engine}
To read a vehicle from a String, we just need to run the automatically supplied FromJSON.parseJSON function:
case parseJSON "{\"wheels\" : 3 }" of Just Vehicle{wheels=w} -> -- we have w wheels ...
Like with FromJSON.fromJSON, the FromJSON.parseJSON function needs to know the type of the item to decode as well as the failure monad to work with. Most often, this can be inferred by the compiler, like in the example above where it is clear that we want a Maybe Vehicle.
Sometimes memory and time are extremely scarce, and the data to decode are comparatively simple. This is the time to roll your own custom JSON parser.
The parsers provided in this module fall into 4 classes:
All Parsers supported by this module operate on list of Tokens. The function lexer can be used to turn a String into such a list, obeying the JSON lexical syntax. Finally, the function runParser actually applies a parser to a token list.
So the skeleton of your code should read like this example:
run :: MonadFail m => String -> m [Float] run = runParser custom . lexer where custom = parseList parseFloat
The "custom parser" here parses a list of floating point values, and on success returns a [Float] in the failure monad of your choice.
lexical entities that can appear in a JSON String
left brace {
right brace }
left bracket [
right bracket ]
colon :
comma ,
lexical error indicator
false
a JSON number, atod will work
null (o noes!!!)
a JSON string, escapes already decoded
true
access field nval
access field offset
access field sval
access field text
Translate a String into a lazy list of Tokens.
There will be at most one Token.ERROR token in the result, and it will be the last in the list. This is, the lexer will stop on the first lexical error.
Absence of lexical errors does not mean valid JSON, for example
"null, true,"
is lexed as
[NULL, COMMA, TRUE, COMMA]
but is, of course invalid.
Internal representation of JSON data
[ value, ... ]
true or false
just null
a number
a string
{ "name":value, ... }
singleton JSON false constant
singleton JSON true constant
Make a single association
Make a Value.Struct with just a single association, suitable for variants
Extract a required field from an association list or MonadFail.fail with an appropriate message.
Convenience function for optional fields, use like
foo ← optional "foo" s
This will decode any of the following
{ ... } -- no "foo" in structure, hence Nothing
{ ..., "foo" : null, ... } -- again, foo is Nothing
{ ..., "foo" : {"Nothing" : null}, ...} --- again Nothing
{ ..., "foo" : 42, ...} -- foo is Just 42
{ ..., "foo" : {"Just" : 42}} -- foo is Just 42
Convenience function to do FromJSON.fromJSON in the Maybe monad.
Convenience function to do FromJSON.fromJSON in the Either monad.
A JSON parser processes a list of Tokens.
run a Parser and extract the result
Parses an Value.Array or a Value.Struct
This is the parser that will be used in type class FromJSON by default.
This parser fails unless it could consume all input.
parse any JSON value
Parse a JSON number
Parse a JSON boolean
Parse JSON null
Parse a JSON array
Parse a JSON struct
If one and the same key appears multiple times, only the last one will be taken into account.
parse a left bracket
parse a left brace
parse a right bracket
parse a right brace
parse a comma
parse a colon
parse a String
parse an Int
parse a Long
parse an Integer
parse a raw Double
parse a raw Float
parse a Bool
parse a pair "string" : v
parse a list
For example,
parseList parseFloat
produces a [Float] directly
parse a raw tuple
Use like
parseTuple (parseFloat, parseString)
This would produce (Float, String)
Pretty print a Value.
This is used in the Show instance for Value, but can be useful otherwise. For example, to print a large Value directly to IO.stdout without realizing the full intermediate String in memory with a line width of 120 use:
import Lib.PP(prettyIO) prettyIO stdout 120 . valDoc
Class of types whose values can be reconstructed from JSON.
There are two ways to accomplish this:
The usual way is to implement FromJSON.fromJSON and keep the default for FromJSON.parseJSON, which parses a String to a Value and passes that value to FromJSON.fromJSON.
Minimal complete definition: FromJSON.fromJSON
The instances of all standard types that are not encoded as objects will have an implementation of FromJSON.parseJSON that fails immediately. This behaviour is in line with the JSON standard which allows only objects (that is, maps or arrays) to appear on the top level.
(), Bool, (,,,,,,,,,,,,), (,,,,,,,,), (,,,,), (,,), (,), (,,,), (,,,,,,), (,,,,,), (,,,,,,,), (,,,,,,,,,,), (,,,,,,,,,), (,,,,,,,,,,,), (,,,,,,,,,,,,,,,,,,,,), (,,,,,,,,,,,,,,,,), (,,,,,,,,,,,,,,), (,,,,,,,,,,,,,), (,,,,,,,,,,,,,,,), (,,,,,,,,,,,,,,,,,,), (,,,,,,,,,,,,,,,,,), (,,,,,,,,,,,,,,,,,,,), (,,,,,,,,,,,,,,,,,,,,,,), (,,,,,,,,,,,,,,,,,,,,,), (,,,,,,,,,,,,,,,,,,,,,,,,), (,,,,,,,,,,,,,,,,,,,,,,,), (,,,,,,,,,,,,,,,,,,,,,,,,,), Int, Double, Lang.Byte, Char, Float, Either, Lang.Short, JArray, Integer, Long, Maybe, Value, StringJ, []
Interpret a JSON value as a value of the instance type, or MonadFail.fail.
This function must be implemented by all instances.
If one has a non-standard implementation of FromJSON.parseJSON that does not use FromJSON.fromJSON, a conforming, but inefficient implementation would be
fromJSON = parseJSON . show
Parse a String which must contain valid JSON, and interpret as value of the instance type.
The standard implementation uses parseObject to make a JSON Value that is passed on to FromJSON.fromJSON.
The class of types that support their serialization to JSON.
Note that the JSON value for primitive types and Strings yields no valid JSON document when printed.
Lang.Byte, (,,,,,,,,,,,,,,,), (,,,,,,,), (,,,), (,), (), (,,), (,,,,,), (,,,,), (,,,,,,), (,,,,,,,,,,,), (,,,,,,,,,), (,,,,,,,,), (,,,,,,,,,,), (,,,,,,,,,,,,,), (,,,,,,,,,,,,), (,,,,,,,,,,,,,,), (,,,,,,,,,,,,,,,,,,,), (,,,,,,,,,,,,,,,,,), (,,,,,,,,,,,,,,,,), (,,,,,,,,,,,,,,,,,,), (,,,,,,,,,,,,,,,,,,,,,,,), (,,,,,,,,,,,,,,,,,,,,,), (,,,,,,,,,,,,,,,,,,,,), (,,,,,,,,,,,,,,,,,,,,,,), (,,,,,,,,,,,,,,,,,,,,,,,,,), (,,,,,,,,,,,,,,,,,,,,,,,,), Bool, Int, Double, Char, Float, Either, Value, Long, Integer, JArray, Lang.Short, Maybe, StringJ, []
The short encoding for Maybe values.
Produces Value.Null for Maybe.Nothing and the jsonized value for Maybe.Just value.
The FromJSON instance for Maybe is prepared to deal with the different encodings.
inherited from Eq.!=
Function generated for derived instance.
Function generated for derived instance.
inherited from Eq.!=
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
Encoding can be:
{"Just": some-value} {"Nothing": _} null some-value
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from FromJSON.parseJSON
inherited from Show.display
inherited from Show.showChars
inherited from Show.showList
inherited from Show.showsPrec
inherited from Show.showsub
inherited from Show.display
inherited from Show.showChars
inherited from Show.showList
inherited from Show.showsPrec
inherited from Show.showsub
Maybe.toJSON produces one of
{"Nothing" : null }
{"Just" : v }
For a more concise representation see maybeToJSON
Show_Token.showsub, Show_Token.display, Show_Token.show, Token.sval, Token.text, Token.nval
Token.has$text, Token.has$offset, Token.has$nval, Token.has$sval
parseColon, parseComma, parseLbrace, parseLbrack, parseRbrace, parseRbrack
parseArray, parseBoolean, parseNull, parseNumber, parseObject, parseStruct, parseValue
Token.NULL, Token.COMMA, Token.FALSE, Token.BRACKR, Token.BRACER, Token.BRACKL, Token.BRACEL, Token.COLON, Token.TRUE