on master help sign in
Merge pull request #418 from andrewthad/document_json_key

Document json key

commitad5079461db80b1796ebcc8b455b96343adb2b19 authorAdam Bergmark <adam@bergmark.nl> dateThu 9 Jun 2016, 19:26 (10 years ago) parent7109d05c159831
1 file changed+113−2
Data/Aeson/Types/ToJSON.hs+113−2
Data/Aeson/Types/ToJSON.hs+113−2
@@ -310,17 +310,128 @@ instance KeyValue Pair where
310310-- Classes and types for map keys
311311-------------------------------------------------------------------------------
312312
313+-- | Typeclass for types that can be used as the key of a map-like container
314+-- (like 'Map' or 'HashMap'). For example, since 'Text' has a 'ToJSONKey'
315+-- instance and 'Char' has a 'ToJSON' instance, we can encode a value of
316+-- type 'Map' 'Text' 'Char':
317+--
318+-- >>> LBC8.putStrLn $ encode $ Map.fromList [("foo" :: Text, 'a')]
319+-- {"foo":"a"}
320+--
321+-- Since 'Int' also has a 'ToJSONKey' instance, we can similarly write:
322+--
323+-- >>> LBC8.putStrLn $ encode $ Map.fromList [(5 :: Int, 'a')]
324+-- {"5":"a"}
325+--
326+-- JSON documents only accept strings as object keys. For any type
327+-- from @base@ that has a natural textual representation, it can be
328+-- expected that its 'ToJSONKey' instance will choose that representation.
329+--
330+-- For data types that lack a natural textual representation, an alternative
331+-- is provided. The map-like container is represented as a JSON array
332+-- instead of a JSON object. Each value in the array is an array with
333+-- exactly two values. The first is the key and the second is the value.
334+--
335+-- For example, values of type '[Text]' cannot be encoded to a
336+-- string, so a 'Map' with keys of type '[Text]' is encoded as follows:
337+--
338+-- >>> LBC8.putStrLn $ encode $ Map.fromList [(["foo","bar","baz" :: Text], 'a')]
339+-- [[["foo","bar","baz"],"a"]]
340+--
341+-- The default implementation of 'ToJSONKey' chooses this method of
342+-- encoding a key, using the 'ToJSON' instance of the type.
343+--
344+-- To use your own data type as the key in a map, all that is needed
345+-- is to write a 'ToJSONKey' (and possibly a 'FromJSONKey') instance
346+-- for it. If the type cannot be trivially converted to and from 'Text',
347+-- it is recommended that 'ToJSONKeyValue' is used. Since the default
348+-- implementations of the typeclass methods can build this from a
349+-- 'ToJSON' instance, there is nothing that needs to be written:
350+--
351+-- > data Foo = Foo { fooAge :: Int, fooName :: Text }
352+-- > deriving (Eq,Ord,Generic)
353+-- > instance ToJSON Foo
354+-- > instance ToJSONKey Foo
355+--
356+-- That's it. We can now write:
357+--
358+-- >>> let m = Map.fromList [(Foo 4 "bar",'a'),(Foo 6 "arg",'b')]
359+-- >>> LBC8.putStrLn $ encode m
360+-- [[{"fooName":"bar","fooAge":4},"a"],[{"fooName":"arg","fooAge":6},"b"]]
361+--
362+-- The next case to consider is if we have a type that is a
363+-- newtype wrapper around 'Text'. The recommended approach is to use
364+-- generalized newtype deriving:
365+--
366+-- > newtype RecordId = RecordId { getRecordId :: Text}
367+-- > deriving (Eq,Ord,ToJSONKey)
368+--
369+-- Then we may write:
370+--
371+-- >>> LBC8.putStrLn $ encode $ Map.fromList [(RecordId "abc",'a')]
372+-- {"abc":"a"}
373+--
374+-- Simple sum types are a final case worth considering. Suppose we have:
375+--
376+-- > data Color = Red | Green | Blue
377+-- > deriving (Show,Read,Eq,Ord)
378+--
379+-- It is possible to get the 'ToJSONKey' instance for free as we did
380+-- with 'Foo'. However, in this case, we have a natural way to go to
381+-- and from 'Text' that does not require any escape sequences. So, in
382+-- this example, 'ToJSONKeyText' will be used instead of 'ToJSONKeyValue'.
383+-- The 'Show' instance can be used to help write 'ToJSONKey':
384+--
385+-- > instance ToJSONKey Color where
386+-- > toJSONKey = ToJSONKeyText f g
387+-- > where f = Text.pack . show
388+-- > g = text . Text.pack . show
389+-- > -- text function is from Data.Aeson.Encoding
390+--
391+-- The situation of needing to turning function @a -> Text@ into
392+-- a 'ToJSONKeyFunction' is common enough that a special combinator
393+-- is provided for it. The above instance can be rewritten as:
394+--
395+-- > instance ToJSONKey Color where
396+-- > toJSONKey = toJSONKeyText (Text.pack . show)
397+--
398+-- The performance of the above instance can be improved by
399+-- not using 'String' as an intermediate step when converting to
400+-- 'Text'. One option for improving performance would be to use
401+-- template haskell machinery from the @text-show@ package. However,
402+-- even with the approach, the 'Encoding' (a wrapper around a bytestring
403+-- builder) is generated by encoding the 'Text' to a 'ByteString',
404+-- an intermediate step that could be avoided. The fastest possible
405+-- implementation would be:
406+--
407+-- > -- Assuming that OverloadedStrings is enabled
408+-- > instance ToJSONKey Color where
409+-- > toJSONKey = ToJSONKeyText f g
410+-- > where f x = case x of {Red -> "Red";Green ->"Green";Blue -> "Blue"}
411+-- > g x = case x of {Red -> text "Red";Green -> text "Green";Blue -> text "Blue"}
412+-- > -- text function is from Data.Aeson.Encoding
413+--
414+-- This works because GHC can lift the encoded values out of the case
415+-- statements, which means that they are only evaluated once. This
416+-- approach should only be used when there is a serious need to
417+-- maximize performance.
418+
313419class ToJSONKey a where
420+ -- | Provides a way to use a data type as the key for a map-like
421+ -- container.
314422 toJSONKey :: ToJSONKeyFunction a
315423 default toJSONKey :: ToJSON a => ToJSONKeyFunction a
316424 toJSONKey = ToJSONKeyValue (toJSON, toEncoding)
425+ -- | This is similar in spirit to the 'showsList' method of 'Show'.
426+ -- It makes it possible to give 'String' keys special treatment
427+ -- without using @OverlappingInstances@.
317428 toJSONKeyList :: ToJSONKeyFunction [a]
318429 default toJSONKeyList :: ToJSON a => ToJSONKeyFunction [a]
319430 toJSONKeyList = ToJSONKeyValue (toJSON, toEncoding)
320431
321432data ToJSONKeyFunction a
322− = ToJSONKeyText (a -> Text, a -> Encoding)
323− | ToJSONKeyValue (a -> Value, a -> Encoding)
433+ = ToJSONKeyText (a -> Text, a -> Encoding) -- ^ key is encoded to string, produces object
434+ | ToJSONKeyValue (a -> Value, a -> Encoding) -- ^ key is encoded to value, produces array
324435
325436contramapToJSONKeyFunction :: (b -> a) -> ToJSONKeyFunction a -> ToJSONKeyFunction b
326437contramapToJSONKeyFunction h x = case x of