💬

Protocol Buffersでカスタムバリデーションを実装する

2024/09/29に公開

はじめに

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