JsonSchemaについて

こんにちは、黒羽です。 ここしばらく開発しているアプリケーションでは、GUI画面の入力項目やブラウザからWebSocketのメッセージとして渡されたパラメータをJsonSchemaファイルに定義した条件でバリデーションするという実装を行ったので定義ファイルを作成するときに詰まったポイントなどを共有します。   そもそもJsonSchemaってなんやねんってときはググればだいたい解決できる世の中ですが、ものすごくざっくりというとプロパティの検証に必要な条件をJson形式で記述したスキーマ言語です。 以下のJsonはかの有名な「見た目は子供、頭脳は大人」な某小学生のプロフィールを基に書いてみました。
{
    "name": "江戸川コナン",
    "age": 6,
    "birthdayMonth": "May",
    "birthdayDay": 4,
    "sex": "male",
    "job": ["student",
        "detective"
    ]
}
この情報をベースに某小学生の通う小学校に入ることのできる人物を定義してみました。
{
    "$schema": "https://json-schema.org/draft/2019-09/schema",
    "$id": "http://example.com/example.json",
    "type": "object",
    "default": {},
    "required": [
        "name",
        "age",
        "birthdayMonth",
        "birthdayDay",
        "sex",
        "job"
    ],
    "properties": {
        "name": {
            "type": "string",
            "minLength": 1,
        },
        "age": {
            "type": "integer",
            "default": 0,
            "minValue": 5
        },
        "birthdayMonth": {
            "type": "string",
            "enum": [ "Jan", "Feb", "Mar", "Apr", "May","Jun", "Jul", "Aug"
            , "Sep", "Oct", "Nov", "Dec"
            ]
        },
        "birthdayDay": {
            "type": "integer",
            "default": 1,
            "maxValue": 31,
"minValue": 1 }, "sex": { "type": "string", "default": "", "enum": [ "male","female" ] }, "job": { "type": "array", "default": [], "items": { "type": "string", "enum": [ "student", "detective", "parent" ] } } } }
定義した条件としては
  • 名前を1文字以上持っていること
  • 5歳以上であること
  • 誕生月の略称がenumで定義した項目に含まれること
  • 誕生日が1~31の範囲であること
  • 性別が男女のどちらかであること
  • 職業が学生、探偵、保護者のいずれかであること
となります。 主題から脱線しそうなのでこれ以上の深掘りはしませんが某黒ずくめの組織の長髪の兄貴だとか警察関係者は立ち入り禁止、眠りの名探偵は職業が探偵、保護者なので立ち入り許可が出ます。 基本的なデータ型のチェック(type)、数字型の値の範囲の判定(minValue, maxValue)、リスト内の項目合致(enum)と基本的なものであれば上記の定義だけで十分です。 if分岐なども可能なので条件によって子プロパティが動的に変わるプロパティなども定義はできますがかなり記述量が増えます。 ここからは不便な点、詰まった点を共有します。

判定の結果がエラーだった場合において、エラー理由を取得すると日本語以外のメッセージしか取得できない

現状、JsonSchemaのエラーメッセージは日本語に対応しておらず、英語含めて3か国語程度しか選択できないようです。 日本国内での使用が一番想定されている開発中のアプリではユーザーに英語のエラーメッセージを解読してもらうのは忍びないのでJsonSchemaに用意されている予約語以外のフィールド名を使用するとAnnotationsというフィールドにオブジェクトとしてまとめられる仕様を利用して日本語メッセージを返すようにしています。

動的なプロパティを作ろうとすると記述量が膨大になる

if分岐があると上で書きましたがif-thenをJson形式で書くため、素直に書くと記述量がとんでもないことになります。 今回のアプリではcase文のような分岐が必要だったため以下のような雰囲気で記述しました。
"allOf": [
        {
            "if": {
                "properties": {
                    "job": {"const": "student"}
                }
            },
            "then": {
                "properties": {
                    "details": { "$ref": "#/definitions/student" }
                }
            }
        },
        {
            "if": {
                "properties": {
                    "job": { "const": "detective" }
                }
            },
            "then": {
                "properties": {
                   "details": { "$ref": "#/definitions/detective" }
                }
            }
        }
    ]
見慣れないプロパティはドキュメント等を参照してもらうとして、条件としては「jobが”student”だった場合はdefinitionsプロパティに定義したstudentのdetailsプロパティを参照し、jobが”detective”だった場合はdetectiveのdetailsプロパティを参照する、それ以外はdetailsを持たない」となっています。 allOfのおかげでここは比較的に簡潔に書けました。(それでもdefinitions項目に別途色々定義したりする必要があるのでJson自体はカオス。)

Formatのチェックがあるがかなり甘い

文字列が特定の形式にのっとっているかを判定してくれるFormatフィールドがあり、URIやIPアドレスの形式チェックができます。これは便利!!!と思って使っていますが細かいところでNGパターンがチェックからすり抜けます。止む無し。 試した過程であったのは
  • IPアドレス→第4オクテットがなくても正常と判断される
  • URI→「:/」でも正常と判断される
といったものです。他のサイトを調べても「Formatだけじゃ不十分だからPatternできちんと正規表現で潰してね!」みたいな感じだったのでFormatとPatternの併用で潰している箇所があります。(ポート番号はさすがにないけど最大値最小値で判定するのは微妙だったのでここも正規表現を使って潰していたり。)  

終わりに

今回はJsonSchemaについてでした。 入力値の簡単なチェック処理をソースコードにだらっと書くのが嫌い、美しくない!と思う方は試してみると面白いかもしれないです。

文字列の途中の不要な文字列を正規表現で検知して消してみる

こんにちは。くろはです。 今回は意外と使う場面のありそう(個人的な感想)な”文字列途中の可変な不要箇所を正規表現を用いて消す”ということについて書こうかと思います。

背景


私はDBを操作する際のGUIアプリケーションとしてHeidiSQLというアプリケーションを使用しています。
HeidiSQLは単一テーブルから抽出したレコードをInsert文の形式でエクスポートできる機能があるため、
時々この機能を利用して開発環境からマスタデータをローカル環境に移してきて開発を行っています。
ただしここでそのままInsert文を実行してしまうと主キーとなっているID列が開発環境の状態のままテーブルに格納されてしまいます。
たいていは気にしなくてもよいのですがローカル環境も元々はいつぞやの時点の本番環境をコピーしたものなので開発環境のIDをそのまま使うと
自分の行っている改修とは無関係な動作を引き起こしかねない(=正しい結果が得られない)ことにつながるパターンがあったりします。
そこでid列を指定せずにレコードを挿入するとテーブル定義上は連番でIDを振ってくれるのでinsert文からID列と値の指定箇所を消してから挿入、といった作業を時々しています。
10行くらいなら「まぁ手作業でもよいか…」となりますがさすがにそれ以上は労力が見合ってくれません。 というわけで表題の正規表現で一気に指定箇所を置換してしまおうと思った次第です。

結論

このようなSQLが複数あったとしてID列とその値を1度に消します。
INSERT INTO `テスト` (`id`, `○○`, `△△`,`□□□`) VALUES (4401241, 'hoge', 'huga', 'hogehoge');
置換前と置換後の正規表現です。 置換前 (.+)id,(.+) VALUES ([0-9]*,(.+) 置換後 $1$2 VALUES ($3 置換した後のSQLは次のように変更されます。 このようなSQLが複数あったとしてID列とその値を1度に消します。
INSERT INTO `テスト` (`id`, `○○`, `△△`,`□□□`) VALUES (4401241, 'hoge', 'huga', 'hogehoge');
置換前と置換後の正規表現です。 置換前 (.+)id,(.+) VALUES ([0-9]*,(.+) 置換後 $1$2 VALUES ($3 置換した後のSQLは次のように変更されます。  
INSERT INTO `テスト` (`○○`, `△△`,`□□□`) VALUES ('hoge', 'huga', 'hogehoge');
無事ID列と値だけが消えました。  

考え方

正規表現における記号の意味等は割愛しますが置換前のパターン構造を文字で解説すると、 【文頭からidまでの任意の文字の繰り返し】 + 【`id`,】 + 【id,からVALUESまでの任意の文字の繰り返し】+【VALUES ( 【0~9の任意の数字の繰り返し+「,」】】+ 【以降文末までの任意の文字の繰り返し】 という感じです(逆に複雑か….?) 正規表現では”()”でくくると1つのグループとして評価されます。(=文字列とは判定されない) そのため「VALUES ( 」 として「 ( 」をエスケープしてあげないと正しくパターンを判定できません。 置換後の正規表現は結構シンプルで 【1つ目のグループに含まれる文字列】+【2つ目のグループに含まれる文字列】+【VALUES (】+ 【3つ目のグループに含まれる文字列】 というパターン構造になっています。 置換前のパターンの箇所で「”()”でくくるとグループとして評価されます」と書きましたが”()”にはもう一つ、後方参照という役割があります。 後方参照とはざっくりというとカッコ内のパターンに合致する文字列を記憶し、パターン内の変数の値として該当する文字列を返してくれます。 置換後の正規表現パターンの$1~$3がその変数にあたります。 これにより変更が不要な文字列をそのまま元の文字列から持ってくることができます。 青線が今回削除した部分、赤線がかっこでくくったり文字列をそのまま指定して新しく作る文字列として指定している部分です。こうやった方が分かりやすかったかも。 今回はIDの値にあたる部分がVALUES ( の後に来る文字列だったためVALUES ( をわざわざべた書きでパターンとしたけどもっと簡潔に書けたかもしれないと思えてきました。話が脱線しそうなので今回はここまでです。

終わりに

改めて見返すと正規表現のパターンの書き方って難しいけどどうパターンを組むかというのがパズルのようで楽しかったりするんですよね。ただカッコでネストしまくるとあっという間に迷宮の出来上がりなのでできるだけ簡潔に書けるように普段から意識していきたいですね。

Integer型の変数がNullかどうかチェックするのはなぜ?

クロハです。 前回のPHPの初歩的な部分に躓いた話に引き続いて、たぶん今回もプログラム経験の浅い人向けの内容です。

事の発端

唐突なのですがJavaのソースの改修をしているときに次のような条件分岐に改修する必要な場面に遭遇しました。

// Hogehoge.hogeId はInteger型の変数 private boolean IsHoge(Hogehoge hogeEntity) { Integer hogeId = 0; if(hogeEntity.hogeId != null) { hogeId = hogeEntity.hogeId; } /** 以下割愛 **/ }

内容としてはhogeId に0を代入しておいて引数のhogeEntityクラスのhogeIdがNullじゃなければhogeIdに代入するというものです。他のプログラムの書き方を見ていて思いついた処理ですね。 ちなみにこの割愛している部分の処理でDBに接続してレコードを検索するのですがフレームワークの都合でhogeIdがNullだとwhere条件句からhogeIdの指定条件が消えてしまうのでhogeIdが異なっても他の条件に一致していればレコードがヒットしてしまうという事情があり上記の条件分岐と代入処理が必要でした。 この時「int」と「Integer」の違いを知らなかった私は「Integerってつまりintでしょ?nullにしようにも0しか入らなくない?」と最初は思っていました。
その後、「そもそもintにnull入れようとするとNullPointerExceptionが出るから0にすらならない..?」とか色々自分の中でも矛盾が発生したので気になって調べた、というのが今回の事の発端です。

「int」と「Integer」の違いとは

ざっくりと書くと
  • int → プリミティブ型
  • Integer → 参照型(intのラッパークラス)
ここで上記の条件の理由が分かった人はプログラミングを基礎からちゃんと理解できてる人だと思います。(というか「int null なぜ」とかググったりしないですよね..) 話を戻します。 まずラッパークラスについてですがこれも言い換えれば「基本のデータ型やオブジェクトを使いやすくするためにメソッドなどを追加したクラス」です。 あまりいい例が思いつかなかったのですが洗濯機を例にします。 洗濯の工程として 対象を投入する→洗う→干す(乾燥する)→畳む を想定すると 普通の洗濯機(基本のデータ型)では投入された対象を洗う機能しかないとなると乾燥以降の手順を自前で行う必要があります。 しかし乾燥機能付きの洗濯機(洗濯機のラッパークラスとする)であれば洗う→乾燥までの工程を洗濯機側でやってくれるので人間は対象の投入と畳む工程をやればよい、という感じですね。文明の利器バンザイ。 次にプリミティブ型と参照型についてですが、 プリミティブ型→値を持つ 参照型→メモリ上の値が格納されているアドレスを持つ という違いがあります。 こちらの記事 に良い具体例があったので引用させていただくと
int a = 1;
int b = a; // bにはaの値:1が格納される
a = 2;

Systemout.println(a); // 2 が出力される
Systemout.println(b); // 1 が出力される

int[] a = {1, 2, 3};
int[] b = a;
a[0] = 0;

Systemout.println(a[0]); // 0 が出力される
Systemout.println(b[0]); // 0 が出力される
int[] b = a の箇所ではint配列aと同じアドレスを見ているためaの値の変更が反映されていますね。 雑な結論ではありますが値が格納されているアドレスを見ているので「値の入っている場所(アドレス)が指定されていない」という状態が参照型におけるNullなのだと個人的に解釈しました。

終わりに

さて、そろそろ話を締めます。 今回学んだことを踏まえて発端となった条件分岐を考えると 引数のHogehogeエンティティのhogeIdがNullの場合は Hogehoge.hogeId に入る値のアドレスがない=アドレスが指定されていない=Null となることがあり得るということでした。 自分の備忘録的な部分がメインですが同じような疑問を持っていた方の助けになればと思います。

PHP触ったことのないプログラマーが動的な型付けに頭を抱えた話


こんにちは!5月に入社した黒羽(くろは)です!


隔週での更新を目標に開発一同で再開しましたテックブログ、再開後としては2つ目(?)の記事になります。
いきなり社歴の浅い新人が書いているので驚かれる方もいると信じて簡単な自己紹介をしつつ、本題に入っていこうと思います。 
さて、文系の学部を出てプログラマーになって6年目も中盤に差し掛かっておりますが、大学在学中よりも時間の経過が速かった気がします。
今までに取り扱った言語としては以下のような感じです。
  • Java
  • C#
  • Python
  • javascript
  • CSS
  • HTML
  • その他にコマンドプロンプト(世の人々がプログラムと聞いてイメージするであろう『黒い画面に文字がダーッとでる』やつ)で使用するスクリプト類など…
  最後のは言語じゃなくてコマンドの塊じゃん!と言われそう イメージマジックの開発で主に使われている言語はPHPなので入社して初めてPHPを触りました。
今まで触ってきた言語とは書き味や仕様が違っていて日々調べながら習得を進めています。
今日はそんな言語仕様に振り回されてハマってしまった時の原因について調べたので(社内ではそりゃ当たり前だろといわれそうなので未来のPHP初学者に向けて)共有できればと思います。

きっかけ


イメージマジックの各プロジェクトのPHPのソースコードを眺めているとどのソースにも
declare(strict_types=1);
と出てきます。 これは何ぞやと調べてみたところ次の記事に行き当たりました。
この記事の冒頭でこのように記述されていました。
 
 
declare(strict_types=1); とは、PHP7から導入された、厳格な型検査モードの指定構文です。

厳格な型検査モード・・・・?????と思いつつ読み進めてみると更に以下の記述が。
<?php
function add(int $a, int $b): int
{
    return $a + $b;
}

var_dump(add(1.0, 2.0));
この状態で単体実行すると、int(3)が出力されます。
  この時の私の頭の中では1つの考えに支配されていました。
「int型の変数にdouble型を堂々と代入するな!!!!!!」


言語による型付け


あまりに理解ができなくて大声になりましたが気を取り直していきます。
プログラミング言語には開発者が変数や戻り値になど事前にどういう型のデータが入るかを指定する静的な型付けと実行時にコンパイラやインタプリンタがコードを解釈してよしなにデータ型を判断してくれる動的型付けがあります。
PHPは言語仕様として動的な型付けを行っています。
私はJava()静的な型付け)からプログラミングをはじめて、当時の学習用テキストにもデータ型が一致しないとエラー吐くと脅されて教えられました。
そのため、データ型が定義されている変数に異なるデータ型を突っ込まれると困ってしまうわけです。
しかもdouble同士の加算でint型で返ってくるとはこれ如何に。
上記のサンプル関数では$a=1.0≒1(int型)、$b=2.0≒2(int型)としてPHPの方で判断しているようです。
話は逸れましたがdeclare(strict_types=1);を付けてさきほどの関数を実行すると

PHP Fatal error: Uncaught TypeError: Argument 1 passed to add() must be of the type integer, float given, called in /Users/hiraku/sandbox/stricttypes/A.php on line 9 and defined in /Users/hiraku/sandbox/stricttypes/A.php:4 Stack trace: #0 /Users/hiraku/sandbox/stricttypes/A.php(9): add(1, 2) #1 {main} thrown in /Users/hiraku/sandbox/stricttypes/A.php on line 4

とエラーが発生します。
つまりdeclare(strict_types=1);とは動的な型付けをしているPHPで静的な型付けのように振舞わせる宣言という風に解釈できます。
ちなみにこの宣言をしているファイルを呼び出している場合のみ有効であるため、別ファイルからuse文等で参照して同じように関数を呼び出しても緩い型検査のままであるのでint型で返ってきます。
つまり引数を$a=1.0,$b=2.0とする場合、
  • declare(strict_types=1); を宣言しているファイルAのadd関数を呼び出す⇒Typeエラーが発生
  • declare(strict_types=1); を宣言しているファイルAを参照しているがdeclare(strict_types=1); を宣言していないファイルBでファイルAのadd関数を呼び出す⇒int型の戻り値が返却される。 
ということです。

最後に


つまるところ何が言いたかったのかというとカルチャーショックを受けたという話でした。
動的な型付けは手軽な一方で想定していない値が返ってくる可能性があるのでやはり多少面倒でも静的な型付けできっちりとデータ型を指定して書く方が私は好きです。(というかぱっと見で何の値が入ってくるのかソースから読み取れないと後々困ることもある)
最後に余談となりますがこの記事を書いてる間に裏どりがてらに色々調べていたのですが 「動的な型付け」と「型推論」は同じものだと思っていましたが違うもののようです。
ここら辺も少し調べてみようと思います。
ここまで読んでいただきありがとうございました。何かの参考になれば幸いです!