JSON Schemaで組織独自の制約をdbtのyamlに設定する

3行まとめ

  • データに対する期待値を定義するData Reliability Levelという活動を始めています
  • Data Reliability Levelを設定するには「XXXの場合にはAAAの項目は入力が必須」といった条件分岐や項目数も多く、レビューが大変という課題がありました
  • 最近のJSON Schemaは記述力が高いため、上述のような制約も記述することができました

前提: データの出口に対する期待値を明示的に設定したい

データ基盤の開発者とデータの活用者の間には期待値のギャップが埋まれがちです。こうしたギャップをなくすため、データに対する期待値をData Reliability Level*1として定義する活動を開始しています。これはデータの出口側に対する一種のData Contractと捉えることもできると思います。

Data Reliability Levelは大まかに3つの段階(Trusted / Business Insight / Adhoc)を設定しており、Levelが高いほど期待値や水準が高いデータになります。

こうした水準が高いデータを作っていくためには、いくつかの観点でメタデータを適切に設定する必要があります。

課題感: 各Data Reliability Levelに対する設定が適切に設定されているかを確認するレビューが大変

Data Reliability Levelは適切に設定できれば強力な武器になり得る一方、これを運用していくのは少し大変です。具体的には、以下の観点で難しさがあります。

  • 設定項目が多い
    • 運用が可能と思われる程度には項目を絞ってはいるものの、それでも片手では収まらない設定項目数があります
    • 項目の漏れがないかをレビュアーが目視で確認するのは大変ですし、普通に漏れが起き得るでしょう
  • Level毎に設定が必須の項目が異なる
    • 基本的には高水準のものほど設定必須の項目が増えますが、そうないものもあります
    • 例えば「adhocなテーブルはずっと運用はしたくない。deprecation_dateの項目の入力を必須としたい」といった具合です
    • こうしたLevel毎の必須入力項目が異なることはレビューをさらに難しくしてしまいます

解決方法: 各Data Reliability Levelに対する必須入力項目をJSON Schemaで機械的にvalidateする

人間には難しいことは機械にさせましょう。適当なスクリプトをでっち上げてもよいですが、今回は汎用的なツールとして使えるJSON SchemaでData Reliability Levelに対する設定をvalidateさせることにします。dbtのメタデータの記述先はjsonではなくyamlファイルになりますが、check-jsonschemaなどを使えばyamlファイルであってもJSON Schemaを元にvalidateさせることができます。

% check-jsonschema --schemafile my_schema.json --default-filetype yaml models/my_mart/my_model.yml

dbtのymlファイルに対するJSON Schemaをゼロから記述するのは骨が折れますが、幸いなことにdbtが公式でJSON Schemaを出してくれているので、ありがたく使わせてもらいましょう。全体は2000行以上あり割とゴツいですが、modelsに関するところなど、必要な箇所のみをコピペすると大分コンパクトにできます。

細かい箇所はあとで説明するとして、コードの全体感は以下に置いておきます。

Data Reliability Levelに対するJSON Schema(クリックで開きます)

{
    "title": "Data Reliability Level(DRL)用のJSON Schema",
    "type": "object",
    "$schema": "http://json-schema.org/draft-07/schema#",
    "properties": {
        "version": {
            "type": "number",
            "const": 2
        },
        "models": {
            "type": "array",
            "items": {
                "type": "object",
                "properties": {
                    "name": {
                        "type": "string"
                    },
                    "description": {
                        "type": "string"
                    },
                    "columns": {
                        "type": "array",
                        "items": {
                            "$ref": "#/$defs/column_properties"
                        }
                    },
                    "config": {
                        "$ref": "#/$defs/model_configs"
                    },
                    "constraints": {
                        "$ref": "#/$defs/constraints"
                    },
                    "data_tests": {
                        "type": "array",
                        "items": {
                            "$ref": "#/$defs/data_tests"
                        }
                    },
                    "deprecation_date": {
                        "type": "string"
                    },
                    "docs": {
                        "$ref": "#/$defs/docs_config"
                    },
                    "meta": {
                        "$ref": "#/$defs/meta"
                    },
                    ...,
                },
                "additionalProperties": false,
                "required": [
                    "name",
                    "description",
                    "meta"
                ],
                "allOf": [
                    {
                        "if": {
                            "$ref": "#/$defs/data_reliability_level_trusted_data"
                        },
                        "then": {
                            "properties": {
                                "config": {
                                    "type": "object",
                                    "properties": {
                                        "contract": {
                                            "type": "object",
                                            "properties": {
                                                "enforced": {
                                                    "type": "boolean",
                                                    "const": true
                                                }
                                            },
                                            "required": [
                                                "enforced"
                                            ]
                                        }
                                    },
                                    "required": [
                                        "contract"
                                    ]
                                },
                                "columns": {
                                    "items": {
                                        "required": [
                                            "description"
                                        ],
                                        "allOf": [
                                            {
                                                "$ref": "#/$defs/column_must_have_not_null"
                                            }
                                        ]
                                    }
                                }
                            },
                            "required": [
                                "config"
                            ]
                        }
                    },
                    {
                        "if": {
                            "$ref": "#/$defs/data_reliability_level_adhoc"
                        },
                        "then": {
                            "required": [
                                "deprecation_date"
                            ]
                        }
                    }
                ]
            }
        }
    },
    "additionalProperties": false,
    "$defs": {
        "meta": {
            "type": "object",
            "properties": {
                "data_reliability_level": {
                    "enum": [
                        "trusted_data",
                        "business_insight",
                        "adhoc"
                    ]
                },
                "manual_data_usage": {
                    "type": "boolean"
                },
                "direct_source_usage": {
                    "type": "boolean"
                },
                "business_owners": {
                    "$ref": "#/$defs/string_or_array_of_strings"
                }
            },
            "required": [
                "data_reliability_level",
                "manual_data_usage",
                "direct_source_usage",
                "slo"
            ],
            "anyOf": [
                {
                    "if": {
                        "$ref": "#/$defs/data_reliability_level_trusted_data"
                    },
                    "then": {
                        "required": [
                            "business_owners"
                        ]
                    }
                },
                {
                    "if": {
                        "$ref": "#/$defs/data_reliability_level_business_insight"
                    },
                    "then": {
                        "required": [
                            "business_owners"
                        ]
                    }
                },
                {
                    "if": {
                        "$ref": "#/$defs/data_reliability_level_adhoc"
                    },
                    "then": {
                        "required": []
                    }
                }
            ]
        },
        "data_reliability_level_trusted_data": {
            "type": "object",
            "properties": {
                "meta": {
                    "type": "object",
                    "properties": {
                        "data_reliability_level": {
                            "const": "trusted_data"
                        }
                    }
                }
            }
        },
        "data_reliability_level_business_insight": {
            "type": "object",
            "properties": {
                "meta": {
                    "type": "object",
                    "properties": {
                        "data_reliability_level": {
                            "const": "business_insight"
                        }
                    }
                }
            }
        },
        "data_reliability_level_adhoc": {
            "type": "object",
            "properties": {
                "meta": {
                    "type": "object",
                    "properties": {
                        "data_reliability_level": {
                            "const": "adhoc"
                        }
                    }
                }
            }
        },
        "column_properties": {
            ...,
        },
        "column_must_have_not_null": {
            "description": "data_reliability_levelがtrusted_dataの場合、各カラムにdata_testsまたはconstraintsのいずれかにnot_nullが含まれている必要があります",
            "anyOf": [
                {
                    "required": ["constraints"],
                    "properties": {
                        "constraints": {
                            "type": "array",
                            "minItems": 1,
                            "contains": {
                                "type": "object",
                                "properties": {
                                    "type": {
                                        "const": "not_null"
                                    }
                                },
                                "required": ["type"]
                            }
                        }
                    }
                },
                {
                    "required": ["data_tests"],
                    "properties": {
                        "data_tests": {
                            "type": "array",
                            "minItems": 1,
                            "contains": {
                                "anyOf": [
                                    {
                                        "required": ["not_null"],
                                        "type": "object",
                                        "properties": {
                                            "type": "object",
                                            "not_null": {
                                                "type": "object",
                                                "properties": {
                                                    "required": ["where"],
                                                    "type": "object",
                                                    "properties": {
                                                        "where": {
                                                            "type": "string"
                                                        }
                                                    }
                                                }
                                            },
                                            "required": ["config"]
                                        }
                                    }
                                ]
                            }
                        }
                    }
                }
            ]
        },
        "constraints": {
            ...,
        },
        "data_tests": {
            ...,
        }
    }
}

見所

私はJSON Schemaの初歩しか知らなかったので、今回の要件をJSON Schemaで記述できるか不安だったのですが、以下の記事により「最近のJSON Schemaは記述力が十分にあるんだな」と分かったので、見所をメモしておきます。

条件分岐を記述する

前述したように、Data Reliability LevelはLevelによって必須の入力項目が異なります。そのため、条件分岐を記述できることが必須条件となりますが、JSON Schemaでは条件を以下のように記述できます。

以下の例では「adhocだったら、deprecation_dateの項目が必須」という条件分岐を含む必須項目の出し分けを表わしています。

{
    ...,
    "properties": {
        ...,
        "models": {
            "type": "array",
            "items": {
                ...,
                "allOf": [
                    {
                        "if": {
                            "$ref": "#/$defs/data_reliability_level_trusted_data"
                        },
                        "then": {
                            ...,
                        }
                    },
                    {
                        "if": {
                            "$ref": "#/$defs/data_reliability_level_adhoc"
                        },
                        "then": {
                            "required": [
                                "deprecation_date"
                            ]
                        }
                    }
                ]
            }
        }
    }
}

$ref$defsで繰り返しの記述を避ける

Data Reliability LevelのJSON Schemaを書く際に条件分岐が至るところに登場しますが、「adhocだったら」の条件を毎回書くのはダルいです。JSON Schemaでは$defsでサブスキーマを定義し、$refでそれを参照する、ということができます。

例えば以下のような具合です。

  • metaの配下にdata_reliability_levelがあり、設定値としてはtrusted_data / business_insight / adhocの3種類のみを許可する、と定義する
  • metaの配下のdata_reliability_leveltrusted_dataになっている条件をdata_reliability_level_trusted_dataと定義する
{
    ...,
    "$defs": {
        "meta": {
            "type": "object",
            "properties": {
                "data_reliability_level": {
                    "enum": [
                        "trusted_data",
                        "business_insight",
                        "adhoc"
                    ]
                },
            },
         },
        "data_reliability_level_trusted_data": {
            "type": "object",
            "properties": {
                "meta": {
                    "type": "object",
                    "properties": {
                        "data_reliability_level": {
                            "const": "trusted_data"
                        }
                    }
                }
            }
        },
        ...,
        "data_reliability_level_adhoc": {
            "type": "object",
            "properties": {
                "meta": {
                    "type": "object",
                    "properties": {
                        "data_reliability_level": {
                            "const": "adhoc"
                        }
                    }
                }
            }
        },
    }
}

これらを定義することで、前述した$refを使う形で条件などを簡単に参照できます。

    "$ref": "#/$defs/data_reliability_level_adhoc"

anyOfでより柔軟な制約を設定する

「このカラムにはNULLが入らない」という制約を記述する際にdbtのconstraintsは便利です(詳しくはこちらを参照してください)。DWHにBigQueryを使っている場合、テーブルのModeRequiredを設定することができ、「物理的な制約としてそもそもNULLが入らない」ということが表現できます。これはテストにより後付けで「NULLが含まれていないようです」と分かるよりも強い制約になります。

そのため、特に高いData Reliability Levelが要求されるカラムについては、基本的に以下のようなnot_nullconstraintsの制約を必須としたいです。

version: 2
models:
  - name: my_user_model
    config:
      contract:
        enforced: true
    columns:
      - name: user_id
        constraints:
          - type: not_null

しかし、条件によっては「この場合だけはどうしてもNULLが入ることが自然」という場合もありえます。その場合は妥協してconstraintsではなくdata_testsを記述することを必須としますが、この場合はどういう場合にNULLが入るかの条件を記述することを必須としたいです(後からNULLの条件を調査するコストが大きくなるため)。つまり、constraintsが書けない場合であっても、以下のようなyamlの記述を必須としたいです。

version: 2
models:
  - name: my_user_model
    columns:
      - name: complex_column
        data_tests:
          - not_null:
              config:
                where: "..." # NULLになる条件を記述する
          # 以下のようなwhere指定なしの場合はvalidateで弾きたい!
          # - not_null

こういった複雑な制約もJSON Schemaでは記述することができます。anyOfを使うことで、複数の条件のどれかを満たせばよい、という制約を記述できます。それなりに混み入った条件ではありますが、きちんと表現できていて最近のJSON Schemaは表現力が高いんだな、ということが分かりました。

{
    ...,
    "$defs": {
        ...,
        "column_must_have_not_null": {
            "description": "data_reliability_levelがtrusted_dataの場合、各カラムにdata_testsまたはconstraintsのいずれかにnot_nullが含まれている必要があります",
            "anyOf": [
                {
                    "required": ["constraints"],
                    "properties": {
                        "constraints": {
                            "type": "array",
                            "minItems": 1,
                            "contains": {
                                "type": "object",
                                "properties": {
                                    "type": {
                                        "const": "not_null"
                                    }
                                },
                                "required": ["type"]
                            }
                        }
                    }
                },
                {
                    "required": ["data_tests"],
                    "properties": {
                        "data_tests": {
                            "type": "array",
                            "minItems": 1,
                            "contains": {
                                "anyOf": [
                                    {
                                        "required": ["not_null"],
                                        "type": "object",
                                        "properties": {
                                            "type": "object",
                                            "not_null": {
                                                "type": "object",
                                                "properties": {
                                                    "required": ["where"],
                                                    "type": "object",
                                                    "properties": {
                                                        "where": {
                                                            "type": "string"
                                                        }
                                                    }
                                                }
                                            },
                                            "required": ["config"]
                                        }
                                    }
                                ]
                            }
                        }
                    }
                }
            ]
        },
        ...,
    }
}

まとめ

Data Reliability Levelというデータに対する期待値を設定する活動において、入力が必須となるメタデータが出てきました。必須となる条件も混み入ったものがあり、レビューアーの負荷になり得るものでしたが、JSON Schemaをうまく使うことで必須項目が入力されているかをうまくvalidateできるようになりました。

JSON Schemaはもっと簡単なものしか定義できないと思っていましたが、ちゃんと勉強しないといけないなと反省しました...!

*1:一般的な用語ではなく社内で定義した用語になります