Hi I am new to this library and I am currently trying out the new experimental interface. I noticed that the decoders only produce the first error instead of all decoding errors.
When decoding fails for a field, the the decoder returns immediately with the error of that field.
I would like the decoder to continue decoding the remaining fields, collecting all decoding errors. I believe this is the behaviour of the stable implementation. Is there a reason for this change and is there a way to retrieve all errors?
Thank you.
| Software | Version(s) |
| ---------- | ---------- |
| io-ts | 2.2.0 |
| fp-ts | 2.5.3 |
| TypeScript | 3.8.3 |
Can you provide an example?
@merisbahti this is the current behavior
import * as D from 'io-ts/lib/Decoder'
import { isLeft } from 'fp-ts/lib/Either'
import { draw } from 'io-ts/lib/Tree'
export const Person = D.type({
name: D.string,
age: D.number
})
const result = Person.decode({})
if (isLeft(result)) {
console.log(draw(result.left))
}
/*
required property "name"
鈹斺攢 cannot decode undefined, should be string
*/
this is the desired behavior
/*
required property "name"
鈹斺攢 cannot decode undefined, should be string
required property "age"
鈹斺攢 cannot decode undefined, should be number
*/
Is there a reason for this change
@jakejx mainly for simplicity and performance, though I have not a strong opinion on this. I modeled the error type so we can easily switch to multiple errors.
For the sake of discussion let's turn this around, is there a reason for collecting all decoding errors which counterbalances a possible degraded performance?
My current use case is mainly for API request validations, and I would prefer the API to return all validation errors. I have written a custom type() function to collect all errors as a workaround currently.
I raised the issue because I noticed that the new implementation produced different results, so I wanted to make sure I was using the library correctly.
For reference this is what I (naively) came up with:
export function type<A>(
properties: { [K in keyof A]: D.Decoder<A[K]> }
): D.Decoder<A> {
return {
decode: (u) => {
const e = D.UnknownRecord.decode(u);
if (E.isLeft(e)) {
return e;
} else {
const errors = [];
const r = e.right;
const a: Partial<A> = {};
for (const k in properties) {
const e = properties[k].decode(r[k]);
if (E.isLeft(e)) {
const error = Tree.make(k, e.left);
errors.push(error);
} else {
a[k] = e.right;
}
}
const allErrors = fromArray(errors);
return allErrors._tag == 'Some'
? E.left(allErrors.value)
: D.success(a as A);
}
},
};
}
I think in general there is no one-size-fit-all approach, for a decoding library (especially an FP-oriented one) accumulation of errors is a fundamental feature, on the other end compromising performances in all cases may not be the best choice.
Drawing inspiration from the Validation & Either data-type we can pick the right solution for the specific problem keeping the same encoding but swapping the instance, I would really love to see the same kind of approach here.
My own personal interpretation on performance is pretty simple, I don't care about performance if I don't have a problem and when I have a problem I go and optimize locally so I'd argue by default we should accumulate while leaving the option to short-circuit as a secondary thing.
I don't care about performance if I don't have a problem and when I have a problem I go and optimize locally
@mikearnaldi I agree, the default behavior should be collecting all errors
@jakejx v2.2.1 released
Most helpful comment
@mikearnaldi I agree, the default behavior should be collecting all errors