This package lets you build up two-way Coder
structures that concisely specify how values of some Elm type can be both encoded to and decoded from JSON.
If you frequently encode from and decode to the same Elm types, it can be tedious and error prone to define your encoders and decoders separately:
import Json.Encode as Encode
import Json.Decode as Decode exposing (Decoder)
type alias User =
{ name : String
, isAdmin : Bool
}
userDecoder : Decoder User
userDecoder =
Decode.map2 User
(Decode.field "name" Decode.string)
(Decode.field "isAdmin" Decode.bool)
encodeUser : User -> Encode.Value
encodeUser user =
Encode.object
[ ( "name", Encode.string user.name )
, ( "isAdmin", Encode.bool user.isAdmin )
]
If you're encoding and decoding a lot of different kinds of data, this requires a lot of code in different functions that needs to be kept in sync. If you add a field to one of your record types but forget to add it to the type's encoder, the compiler can't help you find the omission and you might end up with bad data that can't be decoded with the corresponding decoder. Fuzz tests work well to prevent this, but they require yet more code to be written to remedy the problem.
With this package, you can instead build up a single Coder
that knows how to both encode and decode:
import Json.Bidirectional as Json
type alias User =
{ name : String
, isAdmin : Bool
}
userCoder : Json.Coder User
userCoder =
Json.object User
|> Json.withField "name" .name Json.string
|> Json.withField "isAdmin" .isAdmin Json.bool
Encoding and decoding is accomplished with the encodeValue
, encodeString
, decodeValue
, and decodeString
functions. If you want to get a Decoder from a Coder, you can use the decoder
function.
Because of the nature of the encoding and decoding processes, this approach is not so great if you are working with JSON that is of a very different structure than its corresponding Elm types.
Also, specifying a bidirectional Coder
for union types with more than one constructor is a bit of a hassle (see the custom
function for an example).
This package is at its best when you have full control over the shape of the JSON that you're encoding and decoding from.
Fuzz tests are a great way to make sure your encoders and decoders are mirror images of each other. Here's a great article on this topic that was also the inspiration for releasing this package:
https://www.brianthicks.com/post/2017/04/24/add-safety-to-your-elm-json-encoders-with-fuzz-testing/
If you use this package to build bidirectional Coders, you won't need as many fuzz tests to ensure consistency, but you will still want them in some cases where it's possible to make mistakes. The Elm compiler will ensure that values encoded by a Coder will be able to be decoded to the original type by the same Coder, but the type system cannot always guarantee that the decoded value will be identical to the original. Listed below are some ways that you can make asymmetrical Coders with this package if you aren't careful. These are situations where you might decide that fuzz tests are still worthwhile.
One way that the encoded and decoded values might not be equal is if you specify object fields out of order. For example:
import Json.Bidirectional as Json
type alias EmailContact =
{ name : String
, email : String
}
emailContactCoder : Json.Coder EmailContact
emailContactCoder =
Json.object EmailContact
-- fields in the wrong order!
|> Json.withField "email" .email Json.string
|> Json.withField "name" .name Json.string
The above Coder will encode { name = "Alice", email = "alice@example.com" }
correctly as {"name": "Alice", "email": "alice@example.com"}
. However, because the two string fields are specified in the wrong order, the EmailContact constructor decodes the "email"
field as its name
and vice-versa.
The bimap
function lets you map both the encoding and decoding processes of a Coder by supplying one function for each direction. Here's a contrived example:
import Json.Bidirectional as Json
type StringPair
= StringPair String String
stringPairCoder : Json.Coder StringPair
stringPairCoder =
Json.tuple (Json.string, Json.string)
|> Json.bimap
(\(StringPair left right) -> (left, right))
(\(left, right) -> StringPair left right)
These mapping functions are just complex enough that you might make a mistake in the implementation:
inconsistentStringPairCoder : Json.Coder StringPair
inconsistentStringPairCoder =
Json.tuple (Json.string, Json.string)
|> Json.bimap
-- the left String is used in both places in the encoding!
(\(StringPair left right) -> (left, left))
(\(left, right) -> StringPair left right)
The custom
function lets you create an arbitrary Coder for any type by supplying an encoding function and Decoder for a single type. This function is most useful for implementing Coders for union types with multiple constructors. Use of the custom
function in this way tends to be the most complex and error-prone way of constructing a Coder that this package makes available, and so fuzz testing custom
Coders is highly recommended.