Typeclass for types that can be used as the key of a map-like
container (like
Map or
HashMap). For example, since
Text has a
ToJSONKey instance and
Char has a
ToJSON instance, we can encode a value of type
Map
Text Char:
>>> LBC8.putStrLn $ encode $ Map.fromList [("foo" :: Text, 'a')]
{"foo":"a"}
Since
Int also has a
ToJSONKey instance, we can
similarly write:
>>> LBC8.putStrLn $ encode $ Map.fromList [(5 :: Int, 'a')]
{"5":"a"}
JSON documents only accept strings as object keys. For any type from
base that has a natural textual representation, it can be
expected that its
ToJSONKey instance will choose that
representation.
For data types that lack a natural textual representation, an
alternative is provided. The map-like container is represented as a
JSON array instead of a JSON object. Each value in the array is an
array with exactly two values. The first is the key and the second is
the value.
For example, values of type '[Text]' cannot be encoded to a string, so
a
Map with keys of type '[Text]' is encoded as follows:
>>> LBC8.putStrLn $ encode $ Map.fromList [(["foo","bar","baz" :: Text], 'a')]
[[["foo","bar","baz"],"a"]]
The default implementation of
ToJSONKey chooses this method of
encoding a key, using the
ToJSON instance of the type.
To use your own data type as the key in a map, all that is needed is
to write a
ToJSONKey (and possibly a
FromJSONKey)
instance for it. If the type cannot be trivially converted to and from
Text, it is recommended that
ToJSONKeyValue is used.
Since the default implementations of the typeclass methods can build
this from a
ToJSON instance, there is nothing that needs to be
written:
data Foo = Foo { fooAge :: Int, fooName :: Text }
deriving (Eq,Ord,Generic)
instance ToJSON Foo
instance ToJSONKey Foo
That's it. We can now write:
>>> let m = Map.fromList [(Foo 4 "bar",'a'),(Foo 6 "arg",'b')]
>>> LBC8.putStrLn $ encode m
[[{"fooName":"bar","fooAge":4},"a"],[{"fooName":"arg","fooAge":6},"b"]]
The next case to consider is if we have a type that is a newtype
wrapper around
Text. The recommended approach is to use
generalized newtype deriving:
newtype RecordId = RecordId { getRecordId :: Text }
deriving (Eq,Ord,ToJSONKey)
Then we may write:
>>> LBC8.putStrLn $ encode $ Map.fromList [(RecordId "abc",'a')]
{"abc":"a"}
Simple sum types are a final case worth considering. Suppose we have:
data Color = Red | Green | Blue
deriving (Show,Read,Eq,Ord)
It is possible to get the
ToJSONKey instance for free as we did
with
Foo. However, in this case, we have a natural way to go
to and from
Text that does not require any escape sequences. So
ToJSONKeyText can be used instead of
ToJSONKeyValue to
encode maps as objects instead of arrays of pairs. This instance may
be implemented using generics as follows:
instance ToJSONKey Color where
toJSONKey = genericToJSONKey defaultJSONKeyOptions
Low-level implementations
The
Show instance can be used to help write
ToJSONKey:
instance ToJSONKey Color where
toJSONKey = ToJSONKeyText f g
where f = Text.pack . show
g = text . Text.pack . show
-- text function is from Data.Aeson.Encoding
The situation of needing to turning function
a -> Text
into a
ToJSONKeyFunction is common enough that a special
combinator is provided for it. The above instance can be rewritten as:
instance ToJSONKey Color where
toJSONKey = toJSONKeyText (Text.pack . show)
The performance of the above instance can be improved by not using
String as an intermediate step when converting to
Text.
One option for improving performance would be to use template haskell
machinery from the
text-show package. However, even with the
approach, the
Encoding (a wrapper around a bytestring builder)
is generated by encoding the
Text to a
ByteString, an
intermediate step that could be avoided. The fastest possible
implementation would be:
-- Assuming that OverloadedStrings is enabled
instance ToJSONKey Color where
toJSONKey = ToJSONKeyText f g
where f x = case x of {Red -> "Red";Green ->"Green";Blue -> "Blue"}
g x = case x of {Red -> text "Red";Green -> text "Green";Blue -> text "Blue"}
-- text function is from Data.Aeson.Encoding
This works because GHC can lift the encoded values out of the case
statements, which means that they are only evaluated once. This
approach should only be used when there is a serious need to maximize
performance.