Open3

yup 小技メモ

特定の field をある条件下で取り除く

strip を使うとobject からその field 自体を取り除くことができる。

https://github.com/jquense/yup#schemastripenabled-boolean--true-schema
const schema = object({
  useThis: number(),
  notThis: string().strip(),
});

schema.cast({ notThis: 'foo', useThis: 4 }); // => { useThis: 4 }

この機能を when と組み合わせることによって、ある条件にそぐわない場合はその field を取り除く 挙動を実現できる。

以下の例では、"isJapanese が true の場合のみ、favoriteSushi を object に含ませたい" 状況を想定している。

const schema = object({
  isJapanese: boolean(),
  favoriteSushi: string().when('isJapanese', {
    is: true,
    then: string().required(),
    // false の場合はこの field を削除
    otherwise: string().strip(),
  }),
});

schema.case({ isJapanese: true, favoriteSushi: 'マグロ' }) // => { isJapanese: true, favoriteSushi: 'マグロ' }
schema.cast({ isJapanese: false, favoriteSushi: 'マグロ' }); // => { isJapanese: false }

cast すると isJapanese が false の場合は、favoriteSushi が object から消えていることが確認できる。

(補足) transform を使って undefned にセットするだけでは不十分なので注意。

object schema を merge

concat() を使用することで object schema を merge できる


const userSchema = yup.object({
  id: yup.string().required(),
  name: yup.string().required(),
});

type User = yup.InferType<typeof userSchema>;
// {
//     id: string;
//     name: string;
// }


const extendedUserSchema = userSchema.concat(
  yup.object({
    imageUrl: yup.string(),
  }),
);

type ExtendedUser = yup.InferType<typeof extendedUserSchema>;
// {
//     id: string;
//     name: string;
//     imageUrl: string | undefined;
// }

nest した要素に required な項目を持つが、自身は optional な object

解決方法は2つ。

1. default(undefined) を使用して、default 値を undefined にする。

const userSchema = yup.object({
  id: yup.string().required(),
  pet: yup
    .object({
      name: yup.string().required(),
    })
    .default(undefined),
});

こちらの方法は README に記載されたやり方なのでおすすめ。

https://github.com/jquense/yup#object-schema-defaults

2. lazy() を使用して、schema を出し分ける。

const userSchema = yup.object({
  id: yup.string().required(),
  pet: yup.lazy((value) => {
    if (!value) return yup.mixed();

    return yup.object({
      name: yup.string().required(),
    });
  }),
});

詳しく説明

nest した要素に required な項目を持つが、自身は optional な object

とは一体どういう状況なのか分かりにくいので、具体例と共に説明する。
以下の条件を満たす user object の schema を例にとって考える。

  • id, pet の二つの field をもつ
  • id は string で required
  • pet は object で optional
    • pet object は name を field にもつ
    • name は string で required

つまり、pet field が存在する場合は name が必須だが、pet field 自体はなくてもOK な user object の schema を作っていく。

// OK
const validUser1 = {
  id: '001',
};

// OK
const validUser2 = {
  id: '001',
  pet: {
    name: 'John',
  },
};

// NG!!!!
const invalidUser = {
  id: '001',
  pet: {}, // name 必須
};

この時、以下のような schema が思いつくが、これだと期待通りには動作してくれない。

const userSchema = yup.object({
  id: yup.string().required(),
  // pet object は optional
  pet: yup.object({
    // name は required  
    name: yup.string().required(),
  }),
});

pet field が存在する場合の validation は期待通りに動作してくれるが、

// pet に name が存在しないのでNG
userSchema.isValidSync({
  id: '001',
  pet: {},
});
// => false 

// 正しいデータ
userSchema.isValidSync({
  id: '001',
  pet: {
    name: 'a',
  },
});
// => true

pet field が存在しない場合、期待通りに動いてくれない。

// 正しいデータなので true を返してほしい
userSchema.isValidSync({
  id: '001',
})
// => false 

これは、yup.object() な field が cast される際の挙動に起因する。

Object schema come with a default value already set, which "builds" out the object shape, a sets any defaults for fields:

yup は validation を実行する前に、"値が存在しない場合にdefault 値をセットする" といった入力値の変換 (cast) を行うのだが、object schema の field の場合、 nest した全ての field に、その field の default 値をセットした状態の object が default 値としてセットされる。

今回のケースで言うと、

userSchema.cast({
  id: '001',
})
// => { id: 'id',  pet: { name: undefined } } 

pet の default 値は { name: undefined } になる。つまり、{ id: '001' }{ id: 'id', pet: { name: undefined } } に 変換された後、validation がかかり、結果、required な name が undefined なので、 validation に引っかかってしまう。

以上を踏まえると、対策方法が見えてくる。

"pet object 自体が存在しない場合は許容したいが pet.name は required にしたい" 、これを実現するためには、

    1. validatiion 実行前にセットされる default 値を object ではなく undefined にしてあげる
    1. validation 時に pet field の値の有無をチェックして schema を出し分けてあげる

このどちらかの対応をすれば良い。

1. validatiion 実行前にセットされる default 値を object ではなく undefined にしてあげる

default() を使用することで default 値を設定できるので、default(undefined) として undefined が defaultt 値として使用されるよう設定してあげればOK。

https://github.com/jquense/yup#schemadefaultvalue-any-schema
const userSchema = yup.object({
  id: yup.string().required(),
  pet: yup
    .object({
      name: yup.string().required(),
    })
    .default(undefined),
});

2. validation 時に pet field の値の有無をチェックして schema を出し分けてあげる

lazy() を使えば、"validation 実行時に schema を変更させる" ことができる。

https://github.com/jquense/yup#lazyvalue-any--schema-lazy

lazy() を利用して userSchema を以下のように書き換えると期待した通りに動作してくれる。
validation 実行時に値が存在しない場合は yup.mixed() を返すことで validation を通し、値が存在する場合は、name が required な object の schema を使用するよう設定している。

const userSchema = yup.object({
  id: yup.string().required(),
  pet: yup.lazy((value) => {
    if (!value) return yup.mixed();

    return yup.object({
      name: yup.string().required(),
    });
  }),
});

// 正しいデータ
userSchema.isValidSync({
  id: 'id',
});
// => true

ログインするとコメントできます