ianstormtaylor/superstruct

How to validate dependent fields?

Closed this issue · 3 comments

Hi! Let's say I have simple example like this:
y type is depending on x field value, while x field has its own validation. How can i access current field values in runtime, to check dependent fields? Usually it's done via passing 2nd argument to validation function like (value, data) => RESULT, where data contains all values.
How this can be achieved with superstruct?

NOTE TO EXAMPLE: there is no data argument passed to y validating function. This won't work.

const struct = superstruct({
  types: {
    x: (v) => v === true,
    y: (v, data) => {
       return data.field_1 === true && v === 'string';
    }
  }
});


const Validator = struct({
  field_1: 'x',
  field_2: 'y'
});

const data = {
  field_1:  true,
  field_2:  'string'
};

I

Use the struct.dynamic API.

Example from the documentation

const map = {
  TEXT: struct({
    content: 'string'
  }),
  IMAGE: struct({
    url: 'string'
  })
}

const Node = struct({
  type: struct.enum(['TEXT', 'IMAGE']),
  value: struct.dynamic((value, parent) => {
    // Make sure your function never comes back with anything but a struct.
    return map[parent.type] || struct('undefined')
  })
})

const Struct = struct({
  nodes: [Node]
})

Hey @drenther How would you condition the schema based on a nested attribute? I was thinking about this today:

What I want is to make lang conditional on the value of country

const semiUsefulNestedSchema = struct({
  a: 'string?',
  b: 'string?',
  c: struct.object({
    e: 'string?',
    f: 'string?',
    g: struct.object({
      country: 'string',
      lang: struct.dynamic((value, parent) => {
        return parent.country === 'USA'
          ? struct.enum(['en'])
          : struct.enum(['es', 'fr', 'zn', 'de', 'nl'])
      })
    })
  })
})

The following example does not work but is merely an example idea that maintains backward compatibility, and allow for the authors of dynamic-schema-functions to have a more ergonomic API that permits absolute addressing of attributes to use when building conditional schemas.

I fully understand that the absolute address space of the schema structure represents the same issues as keys may or may not be present, and that the 'composibility' quality decreases of these dynamic schema functions; however, I feel like I those trade-offs would be worth it as they make the project more approachable for someone wanting to move a way from Joi or ajv etc

const moreUsefulNestedSchema = struct({
  a: 'string?',
  b: 'string?',
  c: struct.object({
    country: 'string',
    e: 'string?',
    f: 'string?',
    g: struct.object({
      lang: struct.dynamic((value, parent, root) => {
        // NOTICE "root" was added - so that 
        //  I can traverse the data structure to pluck out the attributes I need to compare against
        return root.c.country === 'USA'  // root is undeinfed, thus c is not defined
          ? struct.enum(['en'])
          : struct.enum(['es', 'fr', 'zn', 'de', 'nl'])
      })
    })
  })
})
console.log(
  semiUsefulNestedSchema({
    a: 'hello',
    b: 'world',
    c: {
      g: {
        country: 'USA',
        lang: `en` // 'english'
      }
    }
  })
)

console.log(
  moreUsefulNestedSchema({
    a: 'hello',
    b: 'world',
    c: {
      country: 'Mexico',
      g: {
        lang: `es` // 'spanish'
      }
    }
  })
)

Maybe there is another way to do this already??? if so I can happily retract my suggestion as overblown and superfluous.

In case anyone is looking for similar functionality:
I validate dates as follows:

  endDate: struct.dynamic((value, parent) => {
    return struct.function((value) => (new Date(value) >= new Date(parent.startDate)) ? true : 'End date should be after start date.')
  })

If I understood it correctly, the returned string should not be a human-readable format, but this makes it much easier to simply display err.reason instead of handling the error separately.

@ericdmoore
I am not sure about this, but I think you should access country by parent.c.g.country. I think parent refers to the top-level object and not the direct parent of the key (haven't tested it so I am not sure).