目的
RailsでJSONカラムを利用するとき、その大抵はRDSの正規化を正しくやろうとすると煩雑になりすぎるためにとった手段であることが多い(少なくとも私は)。
そういう場合対外においてJSONの形式は煩雑になるのでまともにバリデーションができていなかったり、ドキュメンテーションが難しかったりした。
なので本番のJSONカラムに対してJSON Schemaを利用して「バリデーション」「ドキュメンテーション」の両方を実現していこうという話。
※ 基本的にRDSにおいてJSONカラムを利用するのは私は悪手だと考えています。その中でどうしても現状完全に正規化し切るのは難しいという前提でJSONカラムを選択せざるを得なかった時の対応だと思って読んでいただけるといいと思います。
この記事のゴール
- RailsのJSONカラムに対してバリデーションが入っている
- JSONカラムに対してドキュメンテーションがされている(自動でドキュメントが更新される)
- 上記二つが連動することによって陳腐化しないドキュメンテーションになっている
JSON Schemaについて
今回JSONに対してバリデーション・ドキュメンテーションを行うために JSON Schema を使っていく。
書き口がOpenAPIとほぼ同じなのでOpenAPIを書いたことがある人であればほぼ迷わず書けると思う(多分)
今回は該当のリポジトリに doc/jsonschema/
というディレクトリを作成し、この中にJSONファイルを入れるようにしました。
JSONファイルはJSONカラムの形式ごとに分けて運用しています。
JSONカラムにバリデーションをかける
今回はこちらのgemを利用させてもらいました。
Gemfileに json-schema
を追加。 & bundle install
# Gemfile
gem 'json-schema'
カスタムバリデーターの追加
(エラーメッセージの構築は今回そこまでできてないです。。とはいえ管理画面からでほぼ自分でセットアップするので一旦これでよしとしてます)
class JsonValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
JSON::Validator.validate!(options[:schema], value, strict: true)
rescue JSON::Schema::ValidationError => e
error_message = options[:message] || e.message
record.errors.add(attribute, error_message)
end
end
AutoLoaderのパスが入ってなければ追加(ディレクトリは各プロダクトごとにどうするかは整理してください)
module Hoge
class Application < Rails::Application
...
config.autoload_paths << Rails.root.join("app", "validators")
end
end
Railsのモデルでバリデーションを追加
定数としてJSON Schemaを読み込んでvalidatesを追加。
class Hoge < ApplicationRecord
JSON_SCHEMA = File.open("doc/jsonschema/hoge.json") do |f|
JSON.load(f)
end
validates :hoge, json: { schema: JSON_SCHEMA }
end
JSON Schemaのドキュメンテーション
方法はいろいろありますが以下の観点でサクッとできそうなものを選定してます(必要になったら載せ替えをすればいいと思っているので若干雑に選定してます)
- HTML形式で簡単にhostingできてドキュメントが読める(サーバー立てるほどでもなと思ってます)
- 入れ子構造などを含むJSON Schemaを表現できている
今回はこちらのライブラリを利用させていただきました。
方法としてはGithub Actionからドキュメントを生成してホスティング用のS3にアップロードするようにしています。
ビルド環境の用意
実行にはPythonが必要になります。
今回は当時最新だった 3.10.5
のバージョンを入れてます。(最終的にgithub actionでビルドするのでローカルで作らなくてもまぁ良さそう)
私がpyenv使っているので
3.10.5
json-schema-for-humans
アップロード用のS3バケットの用意
public readでS3を用意(必要最低限の設定にしているので必要に応じて設定は足してください)
resource "aws_s3_bucket" "json_schema_doc_hosting" {
bucket = "{{bucket name}}"
acl = "public-read"
policy = <<POLICY
{
"Id": "PolicyForApiDocBudget",
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::{{bucket name}}/*"
}
]
}
POLICY
website {
index_document = "index.html"
}
}
github actionからS3にアプロードするためのroleを用意
data "aws_iam_policy_document" "github_actions_deployment_json_schema_assume_role_policy" {
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = ["arn:aws:iam::{{AWS account_id}}:oidc-provider/token.actions.githubusercontent.com"]
}
condition {
test = "StringLike"
variable = "token.actions.githubusercontent.com:sub"
values = [
"repo:{{your repo}}:*"
]
}
}
}
resource "aws_iam_role" "github_actions_deployment_for_json_schema" {
name = "gha-iam-role-gha-deployment-json-schema"
assume_role_policy = data.aws_iam_policy_document.github_actions_deployment_json_schema_assume_role_policy.json
}
resource "aws_iam_role_policy_attachment" "github_actions_deployment_for_json_schema" {
role = aws_iam_role.github_actions_deployment_for_json_schema.name
policy_arn = aws_iam_policy.github_actions_deployment_for_json_schema.arn
}
data "aws_iam_policy_document" "github_actions_deployment_for_json_schema_policy" {
version = "2012-10-17"
statement {
actions = [
"s3:PutObject"
]
resources = [
aws_s3_bucket.json_schema_doc_hosting.arn,
"${aws_s3_bucket.json_schema_doc_hosting.arn}/*"
]
}
}
resource "aws_iam_policy" "github_actions_deployment_for_json_schema" {
name = "gha-deployment-json-schema-policy"
policy = data.aws_iam_policy_document.github_actions_deployment_for_json_schema_policy.json
}
resource "aws_iam_openid_connect_provider" "github_actions" {
url = "https://token.actions.githubusercontent.com"
client_id_list = ["sts.amazonaws.com"]
thumbprint_list = [data.tls_certificate.github_actions.certificates.0.sha1_fingerprint]
}
data "tls_certificate" "github_actions" {
url = "https://token.actions.githubusercontent.com/.well-known/openid-configuration"
}
github action で main にマージした時に S3 にアップロード
json-schema-for-humans は Python が必要になるのでインストールして実行
name: Deploy JsonSchema
on:
push:
branches: [main]
permissions:
contents: read
id-token: write
jobs:
deploy-json-schema:
runs-on: ubuntu-18.04
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: 3.10.5
- name: Install Dependencies
working-directory: doc/jsonschema
run: pip install -r requirements.txt
- name: generate Document
working-directory: doc/jsonschema
run: |
mkdir result
generate-schema-doc json_schema.json result/
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-region: ap-northeast-1
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/gha-iam-role-gha-deployment-json-schema
role-session-name: GitHubActions-${{ github.run_id }}
role-duration-seconds: 900
- name: upload
working-directory: doc/jsonschema
run: aws s3 cp ./result/ s3://{{your S3 domain}}/result/ --recursive