Also, not a compile-time check, but one other thing I like to do is add
fuzz tests that verify that my encoders and decoders are actually inverses of each other. I create fuzzers for all of my types, and have a generic
jsonRoundTrips test that takes a fuzzer, an encode function and decode function. It checks that for every value produced by the fuzzer, encoding the value and then decoding succeeds and gives you back the original value. Then the compiler can tell you about any problems in your decoder (since it will force you to provide a value for each field in your record when constructing it), and the tests will tell you about any problems in your encoder (since if you forget a field when encoding, then decoding will fail). Not to mention that the test will also catch other silly errors like
Encode.object [ ( "firstName", Encode.string person.firstName ), ( "lastName", Encode.string person.firstName ) ].