JSON Schema と Ajv を使ったバリデーションの勘所


Gaji-Labo フロントエンドエンジニアの茶木です。

スタートアップのプロダクト開発支援をしている私たちは、ユーザー体験を損なわないバリデーションのあり方や、プロダクト開発の先々を見据えた実装をしたいと考えています。

はじめに

本記事では JSON Schema バリデーターである Ajv を使ったバリデーションについて、実践的な勘所について共有したいと思います。

JSON Schema は単なるJSON なので 必ずしもコードベースに含める必要がなく、API などから取得し、動的に input フィールドを作成するケースなどでも役に立ちます。

JSON Schema とは?

JSON Schema は値の妥当性のルールをJSON 形式で定義する書式です。
以下は、その JSON ドキュメントを JavaScript 用にパースしたものです。

const schema = {
  type: "string",
  pattern: "^[0-9-()]+$",
  errorMessage: {
    pattern: "半角の数字・ハイフン・括弧が利用できます",
  },  
}

この例では 正規表現判定である pattern による妥当性判定が指定されています。他にも文字列長を判断する minLength, maxLength などがあります。また type の種類によって使用できるルールも変わります。

基本例

Ajv を使ってバリデータを生成します。

import Ajv from "ajv";
import AjvErrors from "ajv-errors";

const ajv = new Ajv({ allErrors: true });
AjvErrors(ajv, { singleError: false });

const validator = ajv.compile(schema);

// OK
const goodResult = validator.validate("123-456-789");
console.log(goodReuslt); // true

// NG
const badResult = validator.validate("XYZ");
console.log(badResult, validator.errors[0].message); // false, "半角の数字・ハイフン・括弧が利用できます"

前述の JSON Schema を compile に渡すとバリデーター( validator )が生成されます。

validator.validate で値のバリデーションが行えます。バリデーションに失敗しエラーがあるときは、 validator.erros にエラーの内容が記載されます。

実際の環境で使うときの勘所

object で管理する

基本例では単一の値についてバリデーションを行いましたが、実際の環境では、複数の値がバリデーションの対象になるでしょう。

type: object を指定し properties にキーを記載しネスト構造にできます。

const schema = {
  type: "object",
  required: [foo, bar],
  properties: {
    foo: {
      type: "string",
      pattern: "^[0-9-()]+$",
      errorMessage: {
        pattern: "半角の数字・ハイフン・括弧が利用できます",
      },  
    },
    bar: {
      type: "string",
      maxLength: 10,
      errorMessage: {
        maxLength: "10文字以内で入力してください",
      }, 
    }
  }
}

errorMessage の抽出

validator が持つ errors は、エラーの発生条件や発生箇所など多くの情報を持ちますが、実際のフォーム上にエラーメッセージを表示するには、対応する キー名とエラーメッセージのペアが取得できれば十分なので errors をラップして使いやすくします。

function validate(data): {valid: true} || { valid: false, errorMessages: Record<string, string>} {
  if( validator.validate(data) ) {
    return { valid: true } 
  }
  const errorMessages = validator.errors.reduce((acc, elm) => {
    if (
      "instancePath" in elm &&
      typeof elm.instancePath === "string" &&
      elm.message
    ) {
      // NOTE: instancePathの形式は /foo のようになるため先頭の/を削除
      const name = elm.instancePath.slice(1);
      return { ...acc, [name]: elm.message };
    }
    return acc;
  }, {});
  return { valid: false, errorMessages };
}

ポイントは instancePath/ 区切りでキー名が含まれることを考慮して、キー名とメッセージをペアにすることです。

つまづきポイント required

const schema = {
  type: "object",
  required: [foo, bar],
  properties: {
    foo: { ... },
    bar: { ... }
  }
  errorMessage "未入力の項目があります"
}

ここで一点、つまづきやすいポイントについて解説します。

requiredtype: object に設定できるバリデーションルールですが、通常フロントエンドで想定される required とは若干の違いがあります。

  1. required のエラーは object 要素に対して発生し、各要素ではエラーが発生せず、errorMessage も 各要素に指定できない。
  2. required でエラーとなるのは undefined 。空文はエラーにならない。

ようするに JSON Schema の required は子要素の存在をチェックするものなのですが、通常フロントエンドで想定する required はテキストが空文でないことが条件なのです。

代替方法は minLength: 1not: const: "" を子要素に指定することです。これで、空文をエラーとすることができます。

おわりに

ユーザーインプットとバリデーションは切り離せないものです。バリデーションはプロダクトの安全性を担保します。一方で、提供の仕方次第ではユーザーにとっては煩わしいものにもなりえます。

Gaji-Labo は安全性もユーザー体験も大切に考えて、スタートアップのプロダクト支援を行っていきます。

Gaji-Laboでは、React経験が豊富なフロントエンドエンジニアを募集しています

弊社ではReactの知見で事業作りに貢献したいフロントエンドエンジニアを募集しています。大きな制作会社や事業会社とはひと味もふた味も違うGaji-Laboを味わいに来ませんか?

もちろん、一緒にお仕事をしてくださるパートナーさんも随時募集中です。まずはお気軽に声をかけてください。お仕事お問い合わせや採用への応募、共に大歓迎です!

求人応募してみる!

投稿者 Chaki Hironori

webライターもやってるフロントエンドエンジニアです。Reactは自信があります。またデザイン畑の出身で、気持ちのいいアニメーションやインタラクティブな表現は丁寧に手掛けます。好きなものは中南米の遺跡で、スペイン語が少しできます。