Skip to main content
Decoders v2 has been rewritten to be simpler, smaller, and more efficient. Conceptually, it still works the same way as v1, but some of the APIs have been rewritten in a backward incompatible manner. As such, you may have to make some code changes to migrate to v2.
If you run into issues that aren’t covered by this migration guide, please open an issue and we’re happy to help!

Checklist

1

Install decoders v2

npm install decoders
2

Uninstall deprecated dependencies

If applicable, uninstall debrief and lemons:
npm uninstall debrief lemons
3

Stop calling decoders

Decoders are no longer functions - see instructions
4

Rename decoders

Rewrite patterns/imports for decoders that have been renamed - see instructions
5

Rewrite guard() usage

All uses of guard() need to be rewritten - see instructions
6

Update lemons/debrief imports (if applicable)

7

Flow users: Update $DecoderType

Rewrite use of $DecoderType - see instructions

Stop “calling” decoders

Decoders in 1.x were functions. In 2.x they’re more like classes.
// ❌ 1.x
const result = mydecoder(externalData);

Rename some decoders

Some decoders have been renamed. See the table below. You can do a simple “search & replace” command for these:
Replace this v1 pattern…with this v2 APINotes
map(mydecoder, ...)mydecoder.transform(...)migration instructions
compose(mydecoder, predicate(...))mydecoder.refine(...)migration instructions
describe(mydecoder, ...)mydecoder.describe(...)
either, either3, …, either9either
tuple1, tuple2, … tuple6tuple
dispatchtaggedUnion
url(...)httpsUrl / url (signature has changed)migration instructions

map() is now .transform()

The map() decoder has been renamed to the .transform() decoder method:
// ❌ 1.x
import { string } from 'decoders';
const uppercase = map(string, (s) => s.toUpperCase());

compose() + predicate() is now .refine()

The compose() and predicate() decoders from v1 were mostly used in tandem and used to add additional checks to existing decoders. Predicates have been moved to the .refine() decoder method:
// ❌ 1.x
import { compose, number, predicate } from 'decoders';
const odd = compose(
  number,
  predicate((n) => n % 2 !== 0, 'Must be odd'),
);

Guards are no longer a thing

The concept of “guards” has been removed entirely. The following APIs have been removed:
  • The function guard()
  • The type Guard<T>
  • The helper type GuardType<T>
Instead, all decoders now have a .verify() method which does the exact same thing.
// ❌ 1.x
import { guard, Guard } from 'decoders';

const verify: Guard<Whatever> = guard(whatever);
const value: Whatever = verify(externalData);

Signature of url has changed

The signature of the old url decoder has changed. Compare old vs new.
Key changes to url:
  1. url() is no longer a function taking a list of schemes
  2. url now returns a URL instance, no longer a string
  3. There’s no direct way for decoding custom schemas anymore, but you can reimplement the old decoder yourself
Replace this v1 pattern…with this v2 API
url()httpsUrl
url([])url
url(['git'])Define manually (example)

Rewriting imports from lemons or debrief

This section only applies if you used the lemons or debrief library directly to build custom decoders. If not, just ignore this section.
Decoders used to depend on external libraries lemons and debrief, but this is no longer the case in v2. You no longer have to create Ok/Err results manually when defining your own decoders, nor deal with annotating inputs manually. Take this example decoder, which defines a Buffer decoder:
// ❌ 1.x
import { Err, Ok } from 'lemons/Result';
import { annotate } from 'debrief';
import { Decoder } from 'decoders';

export const buffer: Decoder<Buffer> = (blob) =>
  Buffer.isBuffer(blob) ? Ok(blob) : Err(annotate(blob, 'Must be Buffer'));
In 2.x the ok and err helpers get passed to you by .define(), so you no longer have to import them yourself. Also, notice how you no longer have to manually annotate the blob in simple cases like this. You can simply return a string err message directly.

If you are accessing Result instances directly

The Result<T> type is no longer a class, but a very simple data structure. This plays well with TypeScript as well as helps to tree-shake many unused methods from your bundle. As such, methods previously available on Result instances no longer exist. Direct access of the ok, value, and error properties are now favored. Suggested changes:
import { err, ok } from 'decoders';
Replace this v1 pattern…with this v2 API
result.andThen(f)result.ok ? f(result.value) : result
result.dispatch(f, g)result.ok ? f(result.value) : g(result.error)
result.errValue()result.error
result.expect()removed
result.isErr()!result.ok
result.isOk()result.ok
result.map(f)result.ok ? ok(f(result.value)) : result
result.mapError(g)result.ok ? result : err(g(result.error))
result.toString()removed
result.unwrap()removed (see below)
result.value() ?? xxxdecoder.value(...) ?? xxx
result.value() || xxxdecoder.value(...) || xxx
result.withDefault(xxx)decoder.value(...) ?? xxx
If you’re using result.unwrap() it’s probably because you’re using it like so:
// ❌ 1.x
const result = mydecoder(externalData);
result.unwrap(); // Might throw
You will likely no longer need it, now that the decoder method .verify() exists.

Rewrite use of $DecoderType

Flow users only! The helper type $DecoderType has now been simplified. The $-sign has been removed from the name, and you no longer have to $Call<> it.For TypeScript users no changes are needed.
const mydecoder = array(whatever);

// ❌ Old
type X = $Call<$DecoderType, typeof mydecoder>; // Array<Whatever>

// ✅ New
type X = DecoderType<typeof mydecoder>; // Array<Whatever>

Build docs developers (and LLMs) love