JSON Schema高级教程:基于嵌套属性的条件必填校验

JSON Schema作为一种强大的数据结构描述语言,广泛应用于API请求体、配置文件等场景的数据验证。然而,在面对复杂的业务逻辑时,例如根据某个字段的值来动态地改变其他字段的必填性,尤其是当这些字段处于不同层级时,往往会遇到挑战。本文将详细阐述如何利用JSON Schema的条件关键字,实现基于嵌套属性的顶层属性条件必填校验。

问题场景:基于订单类型的商品列表必填

设想一个订单验证场景,我们有一个Order对象,其中包含attributes嵌套对象,而attributes中又有一个order_type字段。我们的需求是:

  • 当attributes.order_type的值为"ORDER"时,顶层的items属性必须存在且非空。
  • 对于order_type的其他值(如"TRANSFER"、"WITHDRAWAL"等),items属性是可选的。

最初的尝试可能倾向于将条件逻辑放置在attributes属性的定义内部,例如在attributes的allOf数组中添加对items的required校验。然而,这种做法是无效的。JSON Schema中required关键字的作用域是其所在的当前JSON对象。这意味着,如果if/then块位于attributes的定义内部,其then块中的required只能作用于attributes自身的子属性,而无法影响到与attributes同级的items属性。

JSON Schema条件逻辑:if、then与allOf

为了解决这类问题,我们需要借助JSON Schema提供的条件关键字:

  • if: 定义一个子模式作为条件。如果数据实例符合if子模式,则应用then子模式。
  • then: 当if条件满足时,需要应用的数据模式。
  • allOf: 用于组合多个子模式。数据实例必须同时满足allOf数组中的所有子模式才能通过验证。

关键在于,if/then块的放置位置决定了其作用域。要使条件逻辑能够影响到与attributes同级的items属性,if/then块必须放置在顶层(根级别),与properties和required关键字处于同一层级。

解决方案:在根级别应用条件逻辑

正确的做法是,在主Schema的根级别添加一个allOf块,并在其中定义我们的条件逻辑。这个if条件需要能够“看到”并判断嵌套的attributes.order_type字段。

以下是实现上述需求的完整JSON Schema:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "$id": "http://json-schema.org/draft-07/schema#",
  "title": "Validaciones sobre el esquema Order",
  "type": "object",
  "properties": {
    "warehouse_id": {
      "type": [
        "string"
      ]
    },
    "operation_type": {
      "type": [
        "string"
      ]
    },
    "order_id": {
      "type": [
        "number"
      ]
    },
    "items": {
      "type": [
        "array"
      ],
      "minItems": 1,
      "items": {
        "type": [
          "object"
        ],
        "properties": {
          "sku": {
            "type": [
              "string"
            ],
            "minLength": 1,
            "maxLength": 50
          },
          "quantity": {
            "type": [
              "integer"
            ],
            "minimum": 1
          }
        },
        "required": [
          "sku",
          "quantity"
        ],
        "additionalProperties": false
      }
    },
    "attributes": {
      "type": [
        "object"
      ],
      "properties": {
        "order_type": {
          "type": [
            "string"
          ],
          "enum": [
            "ORDER",
            "TRANSFER",
            "WITHDRAWAL",
            "DISPOSAL",
            "FRESH"
          ]
        },
        "etd": {
          "type": [
            "string"
          ],
          "pattern": "^(\\d{4})-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])(?:T)(0[0-9]|1[0-9]|2[0-3]):(0[0-9]|1[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9]):(0[0-9]|1[0-9]|2[0-9]|3[0-9]|4[0-9]|5[0-9])(?:Z)$"
        },
        "volume": {
          "type": [
            "integer", "null"
          ],
          "minimum": 0
        },
        "typology": true
      },
      "required": [
        "order_type"
      ],
      "allOf": [
        {
          "if": {
            "properties": {
              "order_type": {
                "const": "ORDER"
              }
            },
            "required": [
              "order_type"
            ]
          },
          "then": {
            "properties": {
              "order_type": true,
              "etd": true,
              "volume": true,
              "typology": {
                "type": [
                  "string", "null"
                ],
                "enum": [
                  "CONVEYABLE",
                  "NON_CONVEYABLE"
                ]
              }
            },
            "required": [
              "order_type",
              "etd"
            ],
            "additionalProperties": false
          }
        },
        {
          "if": {
            "properties": {
              "order_type": {
                "const": "TRANSFER"
              }
            },
            "required": [
              "order_type"
            ]
          },
          "then": {
            "properties": {
              "order_type": true,
              "etd": true,
              "volume": true
            },
            "required": [
              "order_type",
              "etd"
            ],
            "additionalProperties": false
          }
        },
        {
          "if": {
            "properties": {
              "order_type": {
                "const": "WITHDRAWAL"
              }
            },
            "required": [
              "order_type"
            ]
          },
          "then": {
            "properties": {
              "order_type": true,
              "etd": true,
              "volume": true
            },
            "required": [
              "order_type",
              "etd"
            ],
            "additionalProperties": false
          }
        },
        {
          "if": {
            "properties": {
              "order_type": {
                "const": "DISPOSAL"
              }
            },
            "required": [
              "order_type"
            ]
          },
          "then": {
            "properties": {
              "order_type": true,
              "etd": true,
              "volume": true
            },
            "required": [
              "order_type",
              "etd"
            ],
            "additionalProperties": false
          }
        }
      ]
    }
  },
  "required": [
    "warehouse_id",
    "operation_type",
    "attributes"
  ],
  "additionalProperties": false,
  "allOf": [
    {
      "if": {
        "properties": {
          "attributes": {
            "properties": {
              "order_type": { "const": "ORDER" }
            },
            "required": ["order_type"]
          }
        },
        "required": ["attributes"]
      },
      "then": {
        "required": ["items"]
      }
    }
  ]
}

核心Schema解析

让我们聚焦于解决问题的关键部分——位于根级别的allOf块:

  "allOf": [
    {
      "if": {
        "properties": { // 匹配当前(根)对象的属性
          "attributes": { // 检查名为 "attributes" 的属性
            "properties": {
              "order_type": { "const": "ORDER" } // 进一步检查 "attributes" 内部的 "order_type" 属性,其值必须是 "ORDER"
            },
            "required": ["order_type"] // 确保 "attributes" 对象中包含 "order_type"
          }
        },
        "required": ["attributes"] // 确保根对象中包含 "attributes" 属性
      },