superstruct
superstruct copied to clipboard
Coercions nested inside object doesn't work
Example
const CoercedDate = coerce(date(),string(), (dateString) => {
return new Date(dateString);
});
const UserId = string();
const User = object({
createdAt: CoercedDate,
name: string()
});
const facilityValidator = object({
createdAt: CoercedDate,
user: union(UserId, User),
});
const facility = {
createdAt: '2021-10-10T10:10:10.100Z',
user: {
createdAt: '2021-11-11T11:11:11.111Z', // This should be coerced, but is not
name: 'hal',
},
}
const [error] = validate(facility, facilityValidator, { coerce: true, mask: false });
// "At path: user -- Expected the value to satisfy a union of `string | object`, but received: [object Object]"
I dig into the code a bit, and perhaps in line 151 of utils.ts
129 const ctx: Context = { path, branch }
...
151 for (const failure of struct.validator(value, ctx)) {
152 valid = false
153 yield [failure, undefined]
154 }
The ctx
passed intoi the struct.validator
function should've included coerce
boolean but it doesn't (see line 129)?
We recently switched to superstruct and really like the plain functional api. However, we also also run into this problem described by @PoyangLiu here.
I also tried to dig into the codebase and debug the logic, but with limited success so far.
What I've found out is that this problem might only occur in combination with the union
type.
If I modify your example from above a little bit by omitting the union
, the nested coercion seems to work as it should.
import { coerce, date, string, object, validate } from 'superstruct';
const CoercedDate = coerce(date(), string(), (dateString) => {
return new Date(dateString);
});
const UserId = string();
const User = object({
createdAt: CoercedDate,
name: string(),
});
const facilityValidator = object({
createdAt: CoercedDate,
user: User, //union([UserId, User]),
});
const facility = {
createdAt: '2021-10-10T10:10:10.100Z',
user: {
createdAt: '2021-11-11T11:11:11.111Z',
name: 'hal',
},
};
const validationResult = validate(facility, facilityValidator, {
coerce: true,
mask: false,
});
console.log(validationResult);
results in
[
undefined,
{
createdAt: 2021-10-10T10:10:10.100Z,
user: { createdAt: 2021-11-11T11:11:11.111Z, name: 'hal' } // <- here the date is parsed
}
]
I then tried to further break it down to a more simple example, and finally it seems like the issue must be the combination of union
and object
, where object
contains a coercion.
const simpleValidationWithoutObject = union([CoercedDate])
const simpleValidationWithObject = union([
object({
createdAt: CoercedDate,
}),
])
const simpleValidationWithType = union([
type({
createdAt: CoercedDate,
}),
])
console.log(
validate('2021-11-11T11:11:11.111Z', simpleValidationWithoutObject, {
coerce: true,
})
)
// -> [ undefined, 2021-11-11T11:11:11.111Z ] works as expected
console.log(
validate(
{ createdAt: '2021-11-11T11:11:11.111Z' },
simpleValidationWithObject,
{ coerce: true }
)
)
// -> [ undefined, { createdAt: '2021-11-11T11:11:11.111Z' } ] returns date as string instead of a parsed date object
console.log(
validate(
{ createdAt: '2021-11-11T11:11:11.111Z' },
simpleValidationWithType,
{ coerce: true }
)
)
// -> [ undefined, { createdAt: 2021-11-11T11:11:11.111Z } ] returns parsed date!
As we can see, with type
the nested coercion works as expected!
The crucial difference of object
and type
here seems to be that object
has a coercer
method implementation and type
not. Removing the coercer
from object
seems to solve the issue in that particular case, but now some test cases are failing...
@ianstormtaylor Could you please elaborate a bit on that issue :)
Hey, I believe this is an issue with union
specifically. @yss14 to your question, I think it has to do with union
not having an entries
definition—which allows structs to loop over there children to recurse deeper into the value (objects, arrays, etc.). If a flexible entries
method was added I think this would be solved?