🔍

TypeSpecからJSON Schemaを生成してバリデーションに活用する

に公開

TL;DR

  • TypeSpecからJSON Schemaを生成
  • RailsでMySQLのJSON型カラムのバリデーションに json_schemer gemを使用
  • カスタムバリデータを実装し、ActiveRecordのバリデーションに組み込んだ

はじめに

リーナーの新規事業開発チームにいる @corocn です

TypeSpecを使ってフロントエンド向けのAPIクライアントを生成する事例はよく見かけますが、私たちのチームではそれだけでなく、JSON Schemaの生成とバリデーションにも活用しています。

本記事では、TypeSpecからJSON Schemaを出力し、MySQLのJSON型カラムに格納されるデータのバリデーションを行う実装例を紹介します。

背景

私たちのプロダクトでは以下のような構成を採用しています:

  • TypeSpecでAPIとデータスキーマを一元管理
  • TypeSpecからOpenAPIを生成し、OrvalでTypeScriptのAPIクライアントを自動生成
  • MySQLの一部テーブルでJSON型カラムを使用(柔軟なデータ構造への対応のため)
  • JSON型カラムのデータ整合性をJSON Schemaでバリデーション

全体像
TypeSpecからJSON Schemaを生成し、Railsでバリデーションする全体像

今回は図の右側の矢印部分の話になります。
左側のOpenAPI生成の話は 同僚のめろたんが書いてくれた記事 を見てください。

TypeSpecからJSON Schemaを出力する

TypeSpecの@typespec/json-schemaエミッターを使用することで、TypeScript向けの型定義と同じソースからJSON Schemaを生成できます。

プロジェクト構成

モノレポ構成を採用し、以下のようなディレクトリ構造にしています:

.
├── schema/          # TypeSpec定義
│   ├── api/        # API定義
│   ├── models/     # データモデル定義
│   └── tspconfig.yaml
├── server/          # Rails API
│   ├── @typespec/
│   │   └── json-schema/  # 生成されたJSON Schema
│   └── app/
│       └── validators/
│           └── json_schema_validator.rb
└── frontend/        # React SPA

TypeSpec設定例(tspconfig.yaml)

emit:
  - "@typespec/openapi3"
  - "@typespec/json-schema"

options:
  "@typespec/json-schema":
    emitAllModels: true
    outputDir: "../server/@typespec/json-schema"

TypeSpecモデル定義例

ユーザー設定をJSONで保持するケースを例に説明します。
説明用のサンプルモデルで実際のプロダクトでは使ってません。

model UserPreferences {
  theme: "light" | "dark";
  notifications: {
    email: boolean;
    push: boolean;
  };
  language?: string;
}

Railsでのバリデーション実装

使用ライブラリ

JSON Schemaのバリデーションには json_schemer を使用しています。TypeSpecが生成するJSON Schema 2020-12に対応している点が選定理由です。

よりStar数の多い json-schema は2020-12に対応していないため採用を見送りました(未対応のバージョンを使い続けてハマりました)。

# Gemfile
gem 'json_schemer'

カスタムバリデータの実装例

ActiveRecordのカスタムバリデータとして実装することで、モデルのバリデーションに組み込みます。

# app/validators/json_schema_validator.rb
class JsonSchemaValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    return if value.nil?

    # スキーマファイルのパスを構築
    schema_name = options[:schema]
    raise ArgumentError, 'You must provide a :schema option' unless schema_name

    schema_path = Rails.root.join(schema_name)
    unless schema_path.exist?
      raise ArgumentError, "Schema file not found: #{schema_path}"
    end

    # JSON Schemaバリデータを生成
    schemer = JSONSchemer.schema(schema_path)

    # バリデーション実行
    return if schemer.valid?(value)

    # エラーメッセージを収集
    schemer.validate(value).each do |error|
      record.errors.add(attribute, format_error(error))
    end
  end

  private

  def format_error(error)
    property_name = error['data_pointer'].blank? ?
                   error['data'] :
                   error['data_pointer']&.delete_prefix('/')
    "#{property_name} is invalid"
  end
end

モデルでの使用例

class User < ApplicationRecord
  # preferencesカラムはJSON型
  validates :preferences,
            json_schema: { schema: '@typespec/json-schema/UserPreferences.json' }
end

まとめ

機能開発でやむを得ずJSON型を使う局面がありましたが、JSON Schemaのおかげで安心してデータを永続化することができました。JSON Schemaを直接編集する必要がなく、TypeSpecのモデル定義を変更するだけでスキーマが更新されるため、メンテナンスも容易です。

TypeSpecは単なるAPIクライアント生成ツールとしてだけでなく、データバリデーションの基盤としても活用できる強力なツールです。JSON型を使いながらもデータの整合性を保ちたい場合に、ぜひ試してみてください。

参考リンク

採用

一緒に働いてくれるエンジニア募集してます!

https://careers.leaner.co.jp/engineering

リーナーテックブログ

Discussion