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型を使いながらもデータの整合性を保ちたい場合に、ぜひ試してみてください。
参考リンク
採用
一緒に働いてくれるエンジニア募集してます!
Discussion