superstruct icon indicating copy to clipboard operation
superstruct copied to clipboard

Coercions nested inside object doesn't work

Open PoyangLiu opened this issue 3 years ago • 3 comments

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]"

PoyangLiu avatar Nov 04 '21 06:11 PoyangLiu

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)?

PoyangLiu avatar Nov 04 '21 06:11 PoyangLiu

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 :)

yss14 avatar Mar 02 '22 08:03 yss14

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?

ianstormtaylor avatar Mar 02 '22 17:03 ianstormtaylor