前言

        JSON Schema 是实现全量字段校验最核心、最标准的工具。


简介和安装

概念

        校验接口返回响应的全部字段(更进一步的断言)

校验内容

校验流程

安装 jsonschema

pip install jsonschema

校验方式

在线工具校验

JSON Schema Validator - Newtonsoft

代码校验

实现步骤

1.导包 import jsonschema

2.定义 jsonschema 格式 数据校验规则

3.调用 jsonschema.validate(instance = "json 数据" , schema = "jsonschema 规则")

查验校验结果

校验通过:返回 None

校验失败:


JSON Schema 语法

JSON Schema 重点关键字

关键字 描述
type 表示待校验元素的类型
properties 定义待校验的 JSON 对象中,各个 key-value 对中 value 的限制条件
required 定义待校验的对象中,必须存在的 key
const JSON 元素必须等于指定的内容
pattern 使用正则表达式约束字符串类型数据

type 关键字

作用:约束数据类型

integer —— 整数
string —— 字符串
object —— 对象
array —— 数组
number —— 整数/小数
null —— 空值
boolean —— 布尔值

语法

{
  type : 数据类型
}

properties 关键字

说明:是 type 关键字的辅助,用 type 的值为 object 的场景

作用:指定对象中每个字段的检验规则,可以嵌套使用

required 关键字

作用:校验对象中必须存在的字段。字段必须是关键字,且唯一

语法:

{
  required : [ 字段1 , 字段2 , ...]
}

const 关键字

作用:校验的字段名是一个固定值

{
  "字段" : { "const" : 具体值 }
}

pattern 关键字

作用:指定正则表达式,对字符串进行模糊匹配

基础正则示例:

1 包含字符串hello
2 以字符串开头 ^^hello helloworld
3 以字符串结尾 $:hello$ 中国hello
4 匹配[]内任意1个字符[][0 - 9]匹配任意一个数字 [a - z]匹配任意一个小写字母 [cjfew9823]匹配任意一个
5 匹配指定次数{}[0 - 9]{11}匹配11个数字

语法:

{
  字段”:{pattern :  正则表达式}
}

全量字段校验示例

校验代码示例

import pytest
import jsonschema
from jsonschema import validate

# 定义完整的JSON Schema
user_schema = {
    "$schema": "http://json-schema.org/draft-07/schema#",
    "title": "用户注册信息校验Schema",
    "type": "object",
    "properties": {
        "userType": {
            "type": "string",
            "const": "registered",
            "description": "用户类型必须为固定值'registered'"
        },
        "userId": {
            "type": "string",
            "pattern": "^USER_[A-Z0-9]{8}$",
            "description": "用户ID格式:USER_后跟8位大写字母数字"
        },
        "username": {
            "type": "string",
            "pattern": "^[a-zA-Z][a-zA-Z0-9_]{3,15}$",
            "description": "用户名:字母开头,4-16位字母数字下划线"
        },
        "password": {
            "type": "string",
            "pattern": "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,20}$",
            "description": "密码复杂度校验"
        },
        "email": {
            "type": "string",
            "pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
        },
        "phone": {
            "type": "string", 
            "pattern": "^1[3-9]\\d{9}$"
        },
        "age": {
            "type": "integer",
            "minimum": 18,
            "maximum": 100
        },
        "salary": {
            "type": "number",
            "minimum": 0,
            "exclusiveMinimum": True
        },
        "isActive": {
            "type": "boolean"
        },
        "registrationDate": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z$"
        },
        "userLevel": {
            "type": "integer",
            "const": 1
        },
        "profile": {
            "type": "object",
            "properties": {
                "nickname": {"type": "string", "minLength": 1, "maxLength": 20},
                "avatar": {"type": "string", "pattern": "^https?://.*\\.(jpg|jpeg|png|gif)$"}
            },
            "required": ["nickname"],
            "additionalProperties": False
        }
    },
    "required": [
        "userType", "userId", "username", "password", "email", 
        "age", "isActive", "registrationDate", "userLevel"
    ],
    "additionalProperties": False
}

def validate_user_data(user_data):
    """全量字段校验函数"""
    try:
        validate(instance=user_data, schema=user_schema)
        return True, "校验通过"
    except jsonschema.ValidationError as e:
        return False, f"校验失败: {e.message}"

# 测试数据
valid_user_data = {
    "userType": "registered",
    "userId": "USER_ABCD1234",
    "username": "john_doe123",
    "password": "Passw0rd!",
    "email": "john.doe@example.com",
    "phone": "13800138000",
    "age": 25,
    "salary": 15000.50,
    "isActive": True,
    "registrationDate": "2024-01-15T10:30:00Z",
    "userLevel": 1,
    "profile": {
        "nickname": "John",
        "avatar": "https://example.com/avatar.jpg"
    }
}

# 使用pytest的参数化测试
@user1ize("test_name,test_data,expected_pass", [
    # 有效测试用例
    ("完全有效数据", valid_user_data, True),

    # 必填字段测试
    ("缺少必填字段username", {**valid_user_data, "username": None}, False),
    ("缺少必填字段email", {k: v for k, v in valid_user_data.items() if k != "email"}, False),

    # 数据类型测试
    ("age字段类型错误-字符串", {**valid_user_data, "age": "25"}, False),
    ("age字段类型错误-浮点数", {**valid_user_data, "age": 25.5}, False),
    ("isActive字段类型错误", {**valid_user_data, "isActive": "true"}, False),

    # const约束测试
    ("违反userType的const约束", {**valid_user_data, "userType": "guest"}, False),
    ("违反userLevel的const约束", {**valid_user_data, "userLevel": 2}, False),

    # pattern格式测试
    ("userId格式错误", {**valid_user_data, "userId": "user_123"}, False),
    ("email格式错误", {**valid_user_data, "email": "invalid-email"}, False),
    ("phone格式错误", {**valid_user_data, "phone": "1234567890"}, False),
    ("username格式错误-太短", {**valid_user_data, "username": "ab"}, False),
    ("username格式错误-特殊字符开头", {**valid_user_data, "username": "1john"}, False),

    # 数值范围测试
    ("age小于最小值", {**valid_user_data, "age": 16}, False),
    ("age大于最大值", {**valid_user_data, "age": 101}, False),
    ("salary非正数", {**valid_user_data, "salary": 0}, False),

    # 嵌套对象测试
    ("profile缺少必填nickname", {**valid_user_data, "profile": {"avatar": "https://example.com/img.jpg"}}, False),
    ("profile中avatar格式错误", {**valid_user_data, "profile": {"nickname": "John", "avatar": "invalid-url"}}, False),

    # 额外字段测试
    ("包含额外字段", {**valid_user_data, "extraField": "value"}, False),
])
def test_user_data_validation(test_name, test_data, expected_pass):
    """参数化测试用例 - 测试各种验证场景"""
    is_valid, message = validate_user_data(test_data)

    if expected_pass:
        assert is_valid == True, f"{test_name} 应该通过验证,但实际失败: {message}"
    else:
        assert is_valid == False, f"{test_name} 应该失败验证,但实际通过了"

# 特殊的边界值测试
def test_edge_cases():
    """边界值测试"""
    # 边界年龄测试
    edge_age_cases = [
        (17, False),  # 小于最小值
        (18, True),   # 等于最小值
        (100, True),  # 等于最大值  
        (101, False)  # 大于最大值
    ]

    for age, expected in edge_age_cases:
        test_data = {**valid_user_data, "age": age}
        is_valid, message = validate_user_data(test_data)
        assert is_valid == expected, f"年龄边界测试失败: age={age}, 期望={expected}, 实际={is_valid}"

# 测试详细的错误信息
def test_validation_error_messages():
    """测试错误信息的具体内容"""
    # 测试const约束的错误信息
    invalid_data = {**valid_user_data, "userType": "invalid"}
    is_valid, message = validate_user_data(invalid_data)

    assert not is_valid
    assert "const" in message or "invalid" in message.lower()

    # 测试pattern的错误信息
    invalid_data = {**valid_user_data, "email": "invalid"}
    is_valid, message = validate_user_data(invalid_data)

    assert not is_valid
    assert "pattern" in message or "invalid" in message.lower()

# 使用fixture来准备测试数据
@pytest.fixture
def base_user_data():
    """提供基础的有效用户数据fixture"""
    return valid_user_data.copy()

def test_with_fixture(base_user_data):
    """使用fixture的测试示例"""
    # 修改fixture数据但不影响其他测试
    base_user_data["username"] = "new_user_123"
    is_valid, message = validate_user_data(base_user_data)
    assert is_valid == True

# 运行测试的命令行示例
if __name__ == "__main__":
    # 可以直接使用 pytest 命令运行
    # 或者在代码中运行
pytest.main([__file__, "-v"])

对应的 pytest 配置文件(pytest.ini)

[tool:pytest]
addopts = -v --tb=short
testpaths = .
python_files = test_*.py
python_classes = Test*
python_functions = test_*

运行方式

# 运行所有测试
pytest

# 运行特定文件并显示详细信息
pytest test_user_validation.py -v

# 运行包含特定标记的测试
pytest -k "test_edge_cases" -v

# 生成测试报告
pytest --html=report.html

后记

        我最近跑步的最后冲刺阶段,脑子里竟然会蹦出来” 全量校验字段 “这六个字,大抵是魔性洗脑导致的吧。


↙↙↙阅读原文可查看相关链接,并与作者交流