Protocol Buffersでカスタムバリデーションを実装する
はじめに
Protocol Buffers(以下、Protobuf)は、Googleが開発した効率的なデータシリアライゼーションの仕組みです。シンプルなスキーマ定義からコードを自動生成し、様々なプラットフォームで利用可能です。
しかし、データを取り扱う際にはバリデーションが不可欠です。本記事では、Protobufのカスタムオプションを利用して、バリデーションを自動化する方法について解説します。
※この記事は、Protobufの基礎的な知識がある方を対象としています。
Protobufのカスタムオプション
Protobufでは、FieldOptionsを拡張することで、フィールドごとにバリデーションルールを指定することができます。以下の例では、必須フィールド、文字列長、数値範囲などのバリデーションを定義しています。
#CustomRules.proto
syntax = "proto3";
package App;
import "google/protobuf/descriptor.proto";
import "google/protobuf/wrappers.proto";
extend google.protobuf.FieldOptions {
google.protobuf.Int32Value Maxval = 50001;
google.protobuf.Int32Value Minval = 50002;
google.protobuf.BoolValue Required = 50003;
}
このCustomRules.protoを使うことで、各フィールドにバリデーションルールを適用できます。
実際のメッセージ定義
次に、メッセージ定義の中でカスタムオプションを使用して、各フィールドに対してバリデーションを適用します。
#Request.proto
syntax = "proto3";
package App;
import "CustomRules.proto";
message Request {
string Id = 1 [(Required) = {value: true};
string MeetingId = 2 [(Required) = {value: true};
int32 AttendeeId = 3 [(Minval) = {value: 1}, (Maxval) = {value: 20}];
}
message Response {
int32 StatusCode = 1;
string StatusMessage = 2;
}
この例では、IdとMeetingIdは必須である必要があります。また、AttendeeIdは1以上20以下の範囲内である必要があります。
自動生成コードの作成
次に、これらの定義から自動生成コードを生成します。Protobufのコンパイラであるprotocを使用して、対応するC#コードを生成します。C#コードだけでなく、JavaやGoなどのコードにも対応しています。以下のコマンドを実行して、自動生成されたC#ファイルを取得します。
protoc --csharp_out=./output --proto_path=./proto ./proto/Request.proto ./proto/CustomRules.proto
- --csharp_out=./output: 生成されるC#コードの出力先ディレクトリを指定します。
- --proto_path=./proto: .protoファイルが存在するディレクトリを指定します。
- Request.proto と CustomRules.proto: コンパイル対象のプロトコルバッファファイルです。
これでoutputフォルダ内に、Protobuf定義に対応するC#クラスが自動生成されます。この自動生成コードには、各メッセージタイプに対応するC#クラス(Request.proto→Request.cs、CustomRules.proto→CustomRules.cs)が含まれており、これを使ってメッセージのシリアライズやデシリアライズ、さらにバリデーションを行うことができます。
生成されたコードは、メッセージのフィールドアクセス、シリアライズ、デシリアライズのために使用でき、バリデーションのためのカスタムロジックも適用できます。
バリデーションの実装
C#でカスタムオプションを使ってバリデーションを実装します。以下のコードでは、Protobufの自動生成されたクラスに対してバリデーションを実行するロジックを作成しています。
using App;
using Google.Protobuf;
using System.Collections;
using System.Text.RegularExpressions;
internal class BasicValidator
{
internal static List<string> Validate(IMessage message)
{
var results = new List<string>();
foreach (var field in message.Descriptor.Fields.InDeclarationOrder())
{
var prop = message.GetType().GetProperty(field.PropertyName);
if(prop == null) continue;
dynamic? val = prop.GetValue(message);
// 再帰的にValidation
if (val is IMessage)
{
results.AddRange(Validate(val));
continue;
}
var fieldOption = field.GetOptions();
var required = fieldOption?.GetExtension(ValidationRulesExtensions.Required);
var minVal = fieldOption?.GetExtension(ValidationRulesExtensions.Minval);
var maxVal = fieldOption?.GetExtension(ValidationRulesExtensions.Maxval);
// 必須チェック
if (prop.PropertyType == typeof(string))
{
string? v = val;
if (required != null && string.IsNullOrEmpty(v))
{
results.Add("Error Message Here");
}
continue;
}
else
{
if (required != null && val == null)
{
results.Add($"Failed to Required Check. FieldName = {field.PropertyName}");
}
}
// MaxMinチェック
if (prop.PropertyType.IsValueType)
{
if (minVal != null && val < minVal)
{
results.Add("Error Message Here");
}
if (maxVal != null && val > maxVal)
{
results.Add("Error Message Here");
}
continue;
}
// IEnumerable型の場合
if (val is IEnumerable enumerable)
{
foreach(var v in enumerable)
{
if (v is IMessage)
{
results.AddRange(Validate(v));
}
}
}
}
return results;
}
}
このコードでは、各フィールドに設定されたバリデーションルールをもとに、値をチェックしています。再帰的なバリデーションやリスト型の処理もサポートしています。
実際の使用例
最後に、実際にこのバリデーションを使用してみます。
var req = Request.Parser.ParseJson(jsonString);
var results = BasicValidator.Validate(req);
if (results.Count > 0)
{
Console.WriteLine("Validation failed:");
foreach (var result in results)
{
Console.WriteLine(result);
}
}
else
{
Console.WriteLine("Validation passed.");
}
JSONデータをRequestオブジェクトにパースし、BasicValidator.Validateメソッドでバリデーションを行います。
さらにバリデーションを拡張したい場合は、CustomRules.protoにさらにカスタムルールとそのバリデーションロジックを追加するか、BasicValidatorクラスを継承して拡張することなどが考えられます。
まとめ
Protobufのカスタムオプションを活用することで、スキーマレベルでバリデーションルールを定義し、それに基づいて自動的にデータの検証を行うことができます。このアプローチは、メンテナンス性が高く、再利用可能なバリデーションロジックを簡単に実装できる点が大きなメリットです。
Discussion