ianstormtaylor / superstruct

A simple and composable way to validate data in JavaScript (and TypeScript).
https://docs.superstructjs.org
MIT License
7.02k stars 224 forks source link

List of unions getting mismatching error.type and error.path/value? #62

Closed szhangpitt closed 5 years ago

szhangpitt commented 6 years ago

Hello, I have a following script to showcase something I observed. If I have a list of union in my struct, and if one the of members didn't match a union, the path / value points to the actual mismatching property that caused the union to break, but the type only stops at the union level.

I'm not sure if this is intended behavior. In my real use case I have slightly more complex data structure, and the mismatch made it a little harder for me to figure out where the issue is. Can you advise?

const {struct} = require('superstruct');

const Cat = struct({
  name: 'string',
  meow: 'string',
});

const Dog = struct({
  name: 'string',
  bark: 'string'
});

const Pet = struct.union([Cat, Dog]);

const Home = struct({
  pets: [Pet]
});

const homeData = {
  pets: [
    { name: 'pluto', bark: 'woof' },
    { name: 'hello', meow: 'kitty' },
    { name: 'vlad', flap: 'bat flapping' },
  ]
};

try {
  Home.assert(homeData);
} catch (err) {
  console.error('message =', err.message);
  console.error('type =', err.type);
  console.error('path =', err.path);
  console.error('value =', err.value);
  console.error('errors =', err.errors);
}

This prints out

message = Expected a value of type `{name,meow} | {name,bark}` for `pets.2.flap` but received `bat flapping`.

# points to the union struct
type = {name,meow} | {name,bark}    

# points to the prop that broke the union struct
path = [ 'pets', 2, 'flap' ]

# points to the prop that broke the union struct
value = bat flapping                    
errors = [ { data: { pets: [Array] },
    path: [ 'pets', 2, 'flap' ],
    value: 'bat flapping',
    errors: [Circular],
    type: '{name,meow} | {name,bark}' } ]
szhangpitt commented 6 years ago

FYI if I create an "alias" for my each of my union, I get consistent union-level error:

const {struct, superstruct} = require('superstruct');

const Cat = struct({
  name: 'string',
  meow: 'string',
});

const Dog = struct({
  name: 'string',
  bark: 'string'
});

// create alias for Cat and Dog
const _Cat = superstruct({
  types: {
    'Cat': Cat.test
  }
})('Cat');

const _Dog = superstruct({
  types: {
    'Dog': Dog.test
  }
})('Dog');

// Use the alias in union
const Pet = struct.union([_Cat, _Dog]);

const Home = struct({
  pets: [Pet]
});

const homeData = {
  pets: [
    { name: 'pluto', bark: 'woof' },
    { name: 'hello', meow: 'kitty' },
    { name: 'vlad', flap: 'bat flapping' },
  ]
};

try {
  Home.assert(homeData);
} catch (err) {
  console.error('message =', err.message);
  console.error('type =', err.type);
  console.error('path =', err.path);
  console.error('value =', err.value);
  console.error('errors =', err.errors);
}

Getting

message = Expected a value of type `Cat | Dog` for `pets.2` but received `[object Object]`.
type = Cat | Dog
path = [ 'pets', 2 ]
value = { name: 'vlad', flap: 'bat flapping' }
errors = [ { type: 'Cat | Dog',
    value: { name: 'vlad', flap: 'bat flapping' },
    data: { pets: [Array] },
    path: [ 'pets', 2 ],
    errors: [Circular] } ]
ianstormtaylor commented 6 years ago

@szhangpitt good catch! Would you mind submitting a PR with either a broken test case or a full fix? Thank you!