io-ts
io-ts copied to clipboard
How to properly decode prefixed required properties ?
Hi everyone,
My team and I are using io-ts
for a long-time and we like it !
Thanks for all the amazing job.
But we are currently facing a decoding case we are not able to fix. Here is the object we try to decode / validate :
{
"warehouse_name": "held-story-warehouse",
"shelf_stories": "to-maestro,to-search,to-search,to-rating-story",
"warehouse_lifespan": 200,
"switch_name": "switch_none"
}
-
warehouse_name
is a required property ; -
warehouse_lifespan
is an optional property ; -
switch_name
is an extra parameter that should be ignored ; - and we also want to accept keys prefixed with
shelf_
(at least one is required). We could have many of them inside the parsed object.
So here is how we tried to achieve those behaviors and the related errors :
import * as t from 'io-ts';
interface ShelfKeyBrand {
readonly ShelfKey: unique symbol;
}
type ShelfKey = t.Branded<string, ShelfKeyBrand>;
const shelfKey = t.brand(
trimmedString,
(key): key is ShelfKey => key.startsWith('shelf_'),
'ShelfKey',
);
const validator = t.intersection([
t.type({
warehouse_name: t.string,
}),
t.record(shelfKey, t.string),
t.partial({
warehouse_lifespan: t.number,
}),
]);
Expecting ShelfKey at 1.warehouse_name but instead got: warehouse_name.,
Expecting ShelfKey at 1.warehouse_lifespan but instead got: warehouse_lifespan.,
Expecting ShelfKey at 1.switch_name but instead got: switch_name.
The record codec does not seems to properly work with the others contained inside the intersection. Is there a way to reach this desired behavior ?
Thanks for your precious help. Clément
The problem is that in an intersection
every type need to be valid for the given object.
My recommendation for this case would be to parse the object in several step. First parse the whole object via t.record(t.string, t.unknown)
then partition the object in two sets. One that contains your shelf
keys and the other that contains the rest static properties. Then validate them separately and combine the result afterwarts.
Thanks for your quick response and your suggestion, Malte. In fact, we also use this validator to automatically generate documentation of the expected contract, making it always up to date this way
Make use of your suggestion would break this part. But it doesn't seems possible to express our constraints using current io-ts codecs.
So, I guess we will have to make some compromise here...
Moreover, I think the case is maybe only related to record
codec as we face a similar issue with a simpler case.
Given the following object to decode :
{
"priority_age": 10,
"priority_name": 4,
"extra_property": "foo"
}
We could express our constraints using :
import * as t from 'io-ts';
interface PriorityKeyBrand {
readonly PriorityKey: unique symbol;
}
type PriorityKey = t.Branded<string, PriorityKeyBrand>;
const priorityKey = t.brand(
trimmedString,
(key): key is PriorityKey => key.startsWith('priority_'),
'PriorityKey',
);
const validator = t.intersection([
t.record(priorityKey, t.number),
]);
But we are not able to strip extra properties as the exact
or strict
codecs only work with props...
It seems we could want to express some exclusion rules on records...