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.boolEncoding 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.stringThe 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.