Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Strip unknown keys from validated object #132

Open
shane-lab opened this issue Mar 12, 2020 · 2 comments
Open

Strip unknown keys from validated object #132

shane-lab opened this issue Mar 12, 2020 · 2 comments

Comments

@shane-lab
Copy link

shane-lab commented Mar 12, 2020

I'd be nice to take advantage of the validate option in e.g. Record to remove unknown fields when giving an object with known fields and 'valid' values.

Example:

import * as rt from 'runtypes'

const A = rt.Record({ a: number });

const { value } = A.validate({ a: 0, b: 2 }); // = { ..., value: { a: 1, b: 2 } }

// proposal
const { value } = A.validate({ a: 0, b: 2 }, true); // = { ..., value: { a: 1 } }

I hoped to get away with overriding the Record type, but it seems that the validation goes from deepest object to the most outer object when using nested RunTypes.

/**
 * Overloads the validation and strips unknown fields
 * @param {Parameters<typeof rt.Record>} properties
 * @see{rt.Record}
 * @todo fix typings
 */
const Record = properties => {
  const record = rt.Record(properties);

  const originValidate = record.validate.bind(record);

  const fields = Object.keys(record.fields);

  const stipUnknownFields = value =>
    fields.reduce(
      (prev, field) => ({
        ...prev,
        [field]: value[field],
      }),
      {},
    );

  record.validate = model => {
    const validation = originValidate(model);

    if (validation.success) {
      validation.value = stipUnknownFields({ ...validation.value });
    }

    return validation;
  };

  return record;
};

EDIT:

I worked around the fields value, but it would be nice to take into account rt.Array types... I think that's too hacky to implement using this workaround.

const Record = properties => {
  const record = rt.Record(properties);

  const originValidate = record.validate.bind(record);

  const stripUnknownFields = (recordObj, value) => {
    const { tag, underlying, fields = {} } = recordObj;

    if (underlying && underlying.tag === 'record')
      return stripUnknownFields(underlying, value);

    if (tag !== 'record') return value;

    const keys = Object.keys(fields);

    return keys.reduce((prev, key) => {
      const field = fields[key];

      return {
        ...prev,
        [key]: stripUnknownFields(field, value[key]),
      };
    }, {});
  };

  record.validate = model => {
    const validation = originValidate(model);

    if (validation.success) {
      validation.value = stripUnknownFields(record, { ...validation.value });
    }

    return validation;
  };

  return record;
};
@petr-motejlek
Copy link

Hey, I've faced the same conundrum as you. Failing to realize that TypeScript being structurally typed, all objects that have the same (or more) properties will also pass the guard.

Now, technicially speaking, I think that's OK, because that guard (for me) is supposed to be equivalent with what you would get with static typing.

But you can write your own guards, and that's what I was thinking I'll end up doing. I mean, the way I think about this, is that runtypes is a library that gives you the building blocks to model your domain, and if your domain has types such as "partial SOMETHING", "nullable SOMETHING" (I would expect you could give each a normal name (like "incomplete form" for a form that does not have all props), I think writing custom guards, and perhaps packaging them in one "namespace" is the way to go.

@schicks
Copy link
Contributor

schicks commented Jul 18, 2020

Not sure if this is helpful, but I put together a gist that models this as a constraint function. It's a little janky because it needs currying to have access to the runtype it is validating, and only really covers records; however, my experience has been that records are the main thing this is needed for, for things like filtering data coming back from an API. https://gist.github.com/schicks/8a037fe7b3b4d1b0ceb505e8afcc9db3

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants