JSON验证器技能
概述
JSON无处不在 - 配置文件、API、数据存储。无效的JSON会静默破坏系统。在它们到达生产环境之前捕获错误。
核心原则: 尽早验证,经常验证。源头的无效JSON是你代码中的错误。
何时使用
始终:
- 加载配置文件
- 解析API响应
- 提交JSON数据文件之前
- 调试解析错误时
- 验证API输入
- 检查数据完整性
触发短语:
- "验证这个JSON文件"
- "JSON语法错误"
- "检查JSON格式"
- "这个JSON有效吗?"
- "JSON解析失败"
- "格式化JSON"
JSON验证功能
语法检查
- 括号匹配验证
- 引号配对检查
- 逗号位置验证
- 转义字符检查
- Unicode字符验证
结构验证
- 数据类型检查
- 必需字段验证
- 嵌套结构检查
- 数组索引验证
- 对象键名检查
语义分析
- 业务规则验证
- 数据一致性检查
- 范围验证
- 格式验证
- 关联性检查
常见JSON错误
语法错误
问题:
JSON语法不正确
错误示例:
{
"name": "John",
"age": 30, ← 末尾多余的逗号
"city": "New York"
}
解决方案:
移除最后一个属性后的逗号
{
"name": "John",
"age": 30,
"city": "New York"
}
引号错误
问题:
引号使用不正确
错误示例:
{
name: "John", ← 键名需要引号
"message": 'Hello' ← 值必须使用双引号
}
解决方案:
所有键名和字符串值都必须使用双引号
{
"name": "John",
"message": "Hello"
}
数据类型错误
问题:
数据类型不匹配
错误示例:
{
"count": "100", ← 应该是数字
"active": "true", ← 应该是布尔值
"items": null ← 不应该是null
}
解决方案:
使用正确的JSON数据类型
{
"count": 100,
"active": true,
"items": []
}
代码实现示例
JSON验证器
import json
import re
from typing import Dict, List, Any, Optional, Union
from dataclasses import dataclass
from enum import Enum
import jsonschema
class ValidationLevel(Enum):
ERROR = "error"
WARNING = "warning"
INFO = "info"
@dataclass
class ValidationResult:
"""验证结果"""
level: ValidationLevel
message: str
line: Optional[int] = None
column: Optional[int] = None
path: Optional[str] = None
suggestion: Optional[str] = None
@dataclass
class JSONDocument:
"""JSON文档"""
content: Dict[str, Any]
raw_text: str
encoding: str
metadata: Dict[str, Any]
class JSONValidator:
"""JSON验证器"""
def __init__(self):
self.validation_rules = self._initialize_validation_rules()
self.json_schemas = {}
def validate_json_file(self, file_path: str, schema: Optional[Dict[str, Any]] = None) -> List[ValidationResult]:
"""验证JSON文件"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
except Exception as e:
return [ValidationResult(
level=ValidationLevel.ERROR,
message=f"无法读取文件: {str(e)}"
)]
return self.validate_json_content(content, schema)
def validate_json_content(self, content: str, schema: Optional[Dict[str, Any]] = None) -> List[ValidationResult]:
"""验证JSON内容"""
results = []
# 语法验证
syntax_results = self._validate_syntax(content)
results.extend(syntax_results)
# 如果有语法错误,跳过其他验证
if any(r.level == ValidationLevel.ERROR for r in syntax_results):
return results
try:
# 解析JSON
json_data = json.loads(content)
# 结构验证
structure_results = self._validate_structure(json_data)
results.extend(structure_results)
# 模式验证
if schema:
schema_results = self._validate_schema(json_data, schema)
results.extend(schema_results)
# 语义验证
semantic_results = self._validate_semantics(json_data)
results.extend(semantic_results)
# 性能检查
performance_results = self._check_performance(json_data)
results.extend(performance_results)
except json.JSONDecodeError as e:
results.append(ValidationResult(
level=ValidationLevel.ERROR,
message=f"JSON解析错误: {str(e)}",
line=getattr(e, 'lineno', None),
column=getattr(e, 'colno', None)
))
return results
def _validate_syntax(self, content: str) -> List[ValidationResult]:
"""验证语法"""
results = []
lines = content.split('\n')
for line_num, line in enumerate(lines, 1):
# 检查常见的语法问题
# 检查末尾逗号
if re.search(r',\s*[\]\}]', line):
results.append(ValidationResult(
level=ValidationLevel.ERROR,
message="对象或数组末尾有多余的逗号",
line=line_num,
column=line.find(',') + 1,
suggestion="移除末尾的逗号"
))
# 检查未引用的键名
if re.search(r'["']?\s*[a-zA-Z_][a-zA-Z0-9_]*\s*["']?\s*:', line):
match = re.search(r'["']?\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*["']?\s*:', line)
if match and not line.strip().startswith('"'):
results.append(ValidationResult(
level=ValidationLevel.ERROR,
message="JSON键名必须用双引号包围",
line=line_num,
column=line.find(match.group(1)) + 1,
suggestion=f'将 {match.group(1)} 改为 "{match.group(1)}"'
))
# 检查单引号
if "'" in line and not line.strip().startswith('//'):
results.append(ValidationResult(
level=ValidationLevel.ERROR,
message="JSON不允许使用单引号,必须使用双引号",
line=line_num,
column=line.find("'") + 1,
suggestion="将单引号替换为双引号"
))
# 检查注释
if '//' in line.strip() and not line.strip().startswith('"'):
results.append(ValidationResult(
level=ValidationLevel.WARNING,
message="标准JSON不支持注释",
line=line_num,
column=line.find('//') + 1,
suggestion="移除注释或使用JSON5格式"
))
return results
def _validate_structure(self, json_data: Any) -> List[ValidationResult]:
"""验证结构"""
results = []
# 检查数据类型
self._check_data_types(json_data, "", results)
# 检查嵌套深度
max_depth = self._calculate_depth(json_data)
if max_depth > 10:
results.append(ValidationResult(
level=ValidationLevel.WARNING,
message=f"JSON嵌套层级过深 ({max_depth} 层)",
suggestion="考虑扁平化数据结构"
))
# 检查数组大小
self._check_array_sizes(json_data, "", results)
# 检查对象键名
self._check_object_keys(json_data, "", results)
return results
def _check_data_types(self, data: Any, path: str, results: List[ValidationResult]):
"""检查数据类型"""
if isinstance(data, dict):
for key, value in data.items():
current_path = f"{path}.{key}" if path else key
# 检查键名类型
if not isinstance(key, str):
results.append(ValidationResult(
level=ValidationLevel.ERROR,
message=f"对象键名必须是字符串,当前类型: {type(key).__name__}",
path=current_path
))
# 递归检查值
self._check_data_types(value, current_path, results)
elif isinstance(data, list):
for i, item in enumerate(data):
current_path = f"{path}[{i}]" if path else f"[{i}]"
self._check_data_types(item, current_path, results)
def _calculate_depth(self, data: Any, current_depth: int = 0) -> int:
"""计算嵌套深度"""
if isinstance(data, dict):
if not data:
return current_depth
return max(self._calculate_depth(value, current_depth + 1) for value in data.values())
elif isinstance(data, list):
if not data:
return current_depth
return max(self._calculate_depth(item, current_depth + 1) for item in data)
else:
return current_depth
def _check_array_sizes(self, data: Any, path: str, results: List[ValidationResult]):
"""检查数组大小"""
if isinstance(data, dict):
for key, value in data.items():
current_path = f"{path}.{key}" if path else key
self._check_array_sizes(value, current_path, results)
elif isinstance(data, list):
if len(data) > 1000:
results.append(ValidationResult(
level=ValidationLevel.WARNING,
message=f"数组元素过多 ({len(data)} 个)",
path=path,
suggestion="考虑分页或限制数组大小"
))
for i, item in enumerate(data):
current_path = f"{path}[{i}]" if path else f"[{i}]"
self._check_array_sizes(item, current_path, results)
def _check_object_keys(self, data: Any, path: str, results: List[ValidationResult]):
"""检查对象键名"""
if isinstance(data, dict):
# 检查重复键名(JSON解析器会自动处理,但我们可以警告)
keys = list(data.keys())
unique_keys = set(keys)
if len(keys) != len(unique_keys):
results.append(ValidationResult(
level=ValidationLevel.WARNING,
message="对象中存在重复的键名",
path=path,
suggestion="确保键名唯一"
))
# 检查键名格式
for key in keys:
if not re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', key):
results.append(ValidationResult(
level=ValidationLevel.INFO,
message=f"键名 '{key}' 不符合标准命名规范",
path=f"{path}.{key}" if path else key,
suggestion="使用字母、数字和下划线,并以字母或下划线开头"
))
# 递归检查值
for key, value in data.items():
current_path = f"{path}.{key}" if path else key
self._check_object_keys(value, current_path, results)
elif isinstance(data, list):
for i, item in enumerate(data):
current_path = f"{path}[{i}]" if path else f"[{i}]"
self._check_object_keys(item, current_path, results)
def _validate_schema(self, json_data: Any, schema: Dict[str, Any]) -> List[ValidationResult]:
"""验证JSON模式"""
results = []
try:
# 使用jsonschema库验证
jsonschema.validate(json_data, schema)
except jsonschema.ValidationError as e:
results.append(ValidationResult(
level=ValidationLevel.ERROR,
message=f"模式验证失败: {e.message}",
path='.'.join(str(p) for p in e.absolute_path) if e.absolute_path else None,
suggestion="检查数据是否符合预期的模式"
))
except jsonschema.SchemaError as e:
results.append(ValidationResult(
level=ValidationLevel.ERROR,
message=f"JSON模式错误: {e.message}",
suggestion="修复JSON模式定义"
))
return results
def _validate_semantics(self, json_data: Any) -> List[ValidationResult]:
"""验证语义"""
results = []
# 检查常见的语义问题
self._check_null_values(json_data, "", results)
self._check_empty_strings(json_data, "", results)
self._check_numeric_ranges(json_data, "", results)
self._check_date_formats(json_data, "", results)
return results
def _check_null_values(self, data: Any, path: str, results: List[ValidationResult]):
"""检查null值"""
if isinstance(data, dict):
for key, value in data.items():
current_path = f"{path}.{key}" if path else key
if value is None:
results.append(ValidationResult(
level=ValidationLevel.WARNING,
message=f"字段 {current_path} 的值为null",
path=current_path,
suggestion="考虑提供默认值或移除该字段"
))
self._check_null_values(value, current_path, results)
elif isinstance(data, list):
for i, item in enumerate(data):
current_path = f"{path}[{i}]" if path else f"[{i}]"
self._check_null_values(item, current_path, results)
def _check_empty_strings(self, data: Any, path: str, results: List[ValidationResult]):
"""检查空字符串"""
if isinstance(data, dict):
for key, value in data.items():
current_path = f"{path}.{key}" if path else key
if value == "":
results.append(ValidationResult(
level=ValidationLevel.INFO,
message=f"字段 {current_path} 的值为空字符串",
path=current_path,
suggestion="考虑提供有意义的值或移除该字段"
))
self._check_empty_strings(value, current_path, results)
elif isinstance(data, list):
for i, item in enumerate(data):
current_path = f"{path}[{i}]" if path else f"[{i}]"
self._check_empty_strings(item, current_path, results)
def _check_numeric_ranges(self, data: Any, path: str, results: List[ValidationResult]):
"""检查数值范围"""
if isinstance(data, dict):
for key, value in data.items():
current_path = f"{path}.{key}" if path else key
# 检查常见的数值字段
if 'age' in key.lower() and isinstance(value, (int, float)):
if value < 0 or value > 150:
results.append(ValidationResult(
level=ValidationLevel.WARNING,
message=f"年龄值 {value} 超出合理范围",
path=current_path,
suggestion="年龄应该在0-150之间"
))
if 'price' in key.lower() and isinstance(value, (int, float)):
if value < 0:
results.append(ValidationResult(
level=ValidationLevel.WARNING,
message=f"价格值 {value} 不应该为负数",
path=current_path,
suggestion="价格应该为非负数"
))
self._check_numeric_ranges(value, current_path, results)
elif isinstance(data, list):
for i, item in enumerate(data):
current_path = f"{path}[{i}]" if path else f"[{i}]"
self._check_numeric_ranges(item, current_path, results)
def _check_date_formats(self, data: Any, path: str, results: List[ValidationResult]):
"""检查日期格式"""
date_patterns = [
r'^\d{4}-\d{2}-\d{2}$', # YYYY-MM-DD
r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$', # ISO 8601
r'^\d{4}/\d{2}/\d{2}$', # YYYY/MM/DD
]
if isinstance(data, dict):
for key, value in data.items():
current_path = f"{path}.{key}" if path else key
if 'date' in key.lower() and isinstance(value, str):
if not any(re.match(pattern, value) for pattern in date_patterns):
results.append(ValidationResult(
level=ValidationLevel.WARNING,
message=f"日期格式不正确: {value}",
path=current_path,
suggestion="使用标准日期格式如 YYYY-MM-DD"
))
self._check_date_formats(value, current_path, results)
elif isinstance(data, list):
for i, item in enumerate(data):
current_path = f"{path}[{i}]" if path else f"[{i}]"
self._check_date_formats(item, current_path, results)
def _check_performance(self, json_data: Any) -> List[ValidationResult]:
"""检查性能"""
results = []
# 计算JSON大小
json_str = json.dumps(json_data, ensure_ascii=False)
size_bytes = len(json_str.encode('utf-8'))
size_mb = size_bytes / (1024 * 1024)
if size_mb > 10:
results.append(ValidationResult(
level=ValidationLevel.WARNING,
message=f"JSON文件过大 ({size_mb:.2f} MB)",
suggestion="考虑压缩数据或分片处理"
))
# 检查字符串长度
self._check_string_lengths(json_data, "", results)
return results
def _check_string_lengths(self, data: Any, path: str, results: List[ValidationResult]):
"""检查字符串长度"""
if isinstance(data, dict):
for key, value in data.items():
current_path = f"{path}.{key}" if path else key
self._check_string_lengths(value, current_path, results)
elif isinstance(data, list):
for i, item in enumerate(data):
current_path = f"{path}[{i}]" if path else f"[{i}]"
self._check_string_lengths(item, current_path, results)
elif isinstance(data, str):
if len(data) > 10000:
results.append(ValidationResult(
level=ValidationLevel.INFO,
message=f"字符串过长 ({len(data)} 字符)",
path=path,
suggestion="考虑将长文本存储在外部"
))
def _initialize_validation_rules(self) -> Dict[str, Any]:
"""初始化验证规则"""
return {
'max_depth': 10,
'max_array_size': 1000,
'max_file_size_mb': 10,
'max_string_length': 10000
}
def generate_validation_report(self, results: List[ValidationResult]) -> str:
"""生成验证报告"""
if not results:
return "✅ JSON验证通过,未发现问题"
report = ["=== JSON验证报告 ===\n"]
# 按严重程度分组
by_level = {
ValidationLevel.ERROR: [],
ValidationLevel.WARNING: [],
ValidationLevel.INFO: []
}
for result in results:
by_level[result.level].append(result)
for level in [ValidationLevel.ERROR, ValidationLevel.WARNING, ValidationLevel.INFO]:
if by_level[level]:
level_name = level.value.upper()
report.append(f"{level_name} ({len(by_level[level])}):")
for result in by_level[level]:
location = ""
if result.line:
location = f" (第{result.line}行"
if result.column:
location += f", 第{result.column}列"
location += ")"
elif result.path:
location = f" (路径: {result.path})"
report.append(f" - {result.message}{location}")
if result.suggestion:
report.append(f" 建议: {result.suggestion}")
report.append("")
# 总结
error_count = len(by_level[ValidationLevel.ERROR])
warning_count = len(by_level[ValidationLevel.WARNING])
info_count = len(by_level[ValidationLevel.INFO])
summary = f"总结: {error_count} 个错误, {warning_count} 个警告, {info_count} 个信息"
if error_count > 0:
summary += " ❌ 需要修复错误才能使用"
elif warning_count > 0:
summary += " ⚠️ 建议修复警告"
else:
summary += " ✅ JSON格式良好"
report.append(summary)
return '\n'.join(report)
# 使用示例
def main():
validator = JSONValidator()
# 示例JSON内容
json_content = """
{
"name": "John Doe",
"age": 30,
"email": "john@example.com",
"address": {
"street": "123 Main St",
"city": "New York",
"zipcode": "10001"
},
"hobbies": ["reading", "swimming", "coding"],
"active": true
}
"""
# 验证JSON
results = validator.validate_json_content(json_content)
# 生成报告
report = validator.generate_validation_report(results)
print(report)
if __name__ == "__main__":
main()
JSON格式化工具
import json
from typing import Dict, Any, Optional
import yaml
class JSONFormatter:
"""JSON格式化工具"""
def __init__(self):
self.indent_size = 2
self.sort_keys = False
self.ensure_ascii = False
def format_json_file(self, file_path: str, output_path: Optional[str] = None) -> bool:
"""格式化JSON文件"""
try:
# 读取文件
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# 格式化输出
formatted_json = json.dumps(
data,
indent=self.indent_size,
sort_keys=self.sort_keys,
ensure_ascii=self.ensure_ascii
)
# 写入文件
output_file = output_path or file_path
with open(output_file, 'w', encoding='utf-8') as f:
f.write(formatted_json)
return True
except Exception as e:
print(f"格式化失败: {str(e)}")
return False
def minify_json_file(self, file_path: str, output_path: Optional[str] = None) -> bool:
"""压缩JSON文件"""
try:
# 读取文件
with open(file_path, 'r', encoding='utf-8') as f:
data = json.load(f)
# 压缩输出
minified_json = json.dumps(
data,
separators=(',', ':'),
ensure_ascii=self.ensure_ascii
)
# 写入文件
output_file = output_path or file_path
with open(output_file, 'w', encoding='utf-8') as f:
f.write(minified_json)
return True
except Exception as e:
print(f"压缩失败: {str(e)}")
return False
def json_to_yaml(self, json_file: str, yaml_file: str) -> bool:
"""将JSON转换为YAML"""
try:
with open(json_file, 'r', encoding='utf-8') as f:
data = json.load(f)
with open(yaml_file, 'w', encoding='utf-8') as f:
yaml.dump(data, f, allow_unicode=True, default_flow_style=False)
return True
except Exception as e:
print(f"转换失败: {str(e)}")
return False
def yaml_to_json(self, yaml_file: str, json_file: str) -> bool:
"""将YAML转换为JSON"""
try:
with open(yaml_file, 'r', encoding='utf-8') as f:
data = yaml.safe_load(f)
with open(json_file, 'w', encoding='utf-8') as f:
json.dump(data, f, indent=self.indent_size, ensure_ascii=self.ensure_ascii)
return True
except Exception as e:
print(f"转换失败: {str(e)}")
return False
def compare_json_files(self, file1: str, file2: str) -> Dict[str, Any]:
"""比较两个JSON文件"""
try:
with open(file1, 'r', encoding='utf-8') as f:
data1 = json.load(f)
with open(file2, 'r', encoding='utf-8') as f:
data2 = json.load(f)
# 比较内容
if data1 == data2:
return {
"identical": True,
"differences": []
}
else:
# 简单的差异检测
differences = self._find_differences(data1, data2, "")
return {
"identical": False,
"differences": differences
}
except Exception as e:
return {
"identical": False,
"error": str(e)
}
def _find_differences(self, obj1: Any, obj2: Any, path: str) -> List[str]:
"""查找两个对象的差异"""
differences = []
if type(obj1) != type(obj2):
differences.append(f"{path}: 类型不同 ({type(obj1).__name__} vs {type(obj2).__name__})")
elif isinstance(obj1, dict):
keys1 = set(obj1.keys())
keys2 = set(obj2.keys())
# 检查键的差异
if keys1 != keys2:
missing_in_obj2 = keys1 - keys2
missing_in_obj1 = keys2 - keys1
for key in missing_in_obj2:
differences.append(f"{path}.{key}: 只在第一个对象中存在")
for key in missing_in_obj1:
differences.append(f"{path}.{key}: 只在第二个对象中存在")
# 检查共同键的值
common_keys = keys1 & keys2
for key in common_keys:
new_path = f"{path}.{key}" if path else key
differences.extend(self._find_differences(obj1[key], obj2[key], new_path))
elif isinstance(obj1, list):
if len(obj1) != len(obj2):
differences.append(f"{path}: 数组长度不同 ({len(obj1)} vs {len(obj2)})")
else:
for i, (item1, item2) in enumerate(zip(obj1, obj2)):
new_path = f"{path}[{i}]" if path else f"[{i}]"
differences.extend(self._find_differences(item1, item2, new_path))
elif obj1 != obj2:
differences.append(f"{path}: 值不同 ({obj1} vs {obj2})")
return differences
# 使用示例
def main():
formatter = JSONFormatter()
# 格式化JSON文件
success = formatter.format_json_file("data.json")
if success:
print("JSON文件格式化完成")
# 压缩JSON文件
formatter.minify_json_file("data.json", "data.min.json")
# 转换为YAML
formatter.json_to_yaml("data.json", "data.yaml")
# 比较JSON文件
comparison = formatter.compare_json_files("data1.json", "data2.json")
if comparison["identical"]:
print("JSON文件相同")
else:
print("JSON文件不同:")
for diff in comparison["differences"]:
print(f" - {diff}")
if __name__ == "__main__":
main()
JSON最佳实践
文件组织
- 一致格式: 使用统一的缩进和格式
- 合理结构: 避免过深的嵌套
- 命名规范: 使用清晰、一致的键名
- 文档化: 添加必要的注释和说明
数据设计
- 类型一致: 确保相同字段的数据类型一致
- 必需字段: 明确标识必需和可选字段
- 默认值: 为可选字段提供合理的默认值
- 验证规则: 定义数据验证规则
性能优化
- 文件大小: 控制JSON文件大小
- 解析效率: 避免复杂的数据结构
- 压缩传输: 使用gzip压缩传输
- 缓存策略: 实现适当的缓存机制
相关技能
- yaml-validator - YAML验证
- data-validator - 数据验证
- api-validator - API验证
- config-validator - 配置验证