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 フロントエンドエンジニア向けご案内資料

Gaji-Labo は Next.js, React, TypeScript 開発の実績と知見があります

フロントエンド開発の専門家である私たちが御社の開発チームに入ることで、バックエンドも含めた全体の開発効率が上がります。

「既存のサイトを Next.js に移行したい」
「人手が足りず信頼できるエンジニアを探している」
「自分たちで手を付けてみたがいまいち上手くいかない」

フロントエンド開発に関わるお困りごとがあれば、まずは一度お気軽に Gaji-Labo にご相談ください。

オンラインでのヒアリングとフルリモートでのプロセス支援にも対応しています。

Next.js, React, TypeScript の相談をする!

投稿者 Chaki Hironori

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