使用Newtonsoft.Json.Schema验证JSON时未捕获错误的原因排查
使用Newtonsoft.Json.Schema验证JSON时的未捕获错误问题
我们在使用Newtonsoft.Json.Schema进行JSON验证时遇到异常:尽管OpenAPI Schema中mCRO字段的nullable设为false、iNumber字段格式定义为varchar(10),但包含mCRO为null、iNumber超长的Payload JSON验证返回true,未捕获这些错误。
相关代码与Schema
JSON Schema
{ "openapi": "3.0.2", "info": { "title": "Demo API", "version": "1.1.1", "description": "<b>Demo version" }, "tags": [ { "name": "Records", "description": "Registration" } ], "paths": { "/records/AddUpdateRecord": { "post": { "tags": ["Records"], "description": "Add or Update a Object", "requestBody": { "required": true, "content": { "application/json": { "schema": {"$ref": "#/components/schemas/addUpdateRecords"} } } }, "responses": { "200": { "description": "OK - Indicates that the request has succeeded.", "content": { "application/json": { "schema": {"$ref": "#/components/schemas/httpResponse200"} } } }, "400": { "description": "Bad Request", "content": { "application/json": { "schema": {"$ref": "#/components/schemas/httpResponse400"} } } }, "401": {"description": "Unauthorized "}, "403": {"description": "Forbidden - Unauthorized request."}, "429": {"description": "Too Many Requests "}, "500": {"description": "Internal Server Error "} } } }, "/records/AddUpdateC": { "post": { "tags": ["Records"], "description": "Add or Update existing 1 to N Coverages.", "requestBody": { "required": true, "content": { "application/json": { "schema": {"$ref": "#/components/schemas/addUpdateC"} } } }, "responses": { "200": { "description": "OK - Indicates that the request has succeeded.", "content": { "application/json": { "schema": {"$ref": "#/components/schemas/httpResponse200"} } } }, "400": { "description": "Bad Request ", "content": { "application/json": { "schema": {"$ref": "#/components/schemas/httpResponseC400"} } } }, "401": {"description": "Unauthorized ."}, "403": {"description": "Forbidden."}, "429": {"description": "Too Many Requests "}, "500": {"description": "Internal Server Error "} } } }, "/records/UpdateRO": { "put": { "tags": ["Records"], "description": "Update an existing RO.", "requestBody": { "required": true, "content": { "application/json": { "schema": {"$ref": "#/components/schemas/updateRO"} } } }, "responses": { "200": { "description": "OK - Indicates that the request has succeeded.", "content": { "application/json": { "schema": {"$ref": "#/components/schemas/httpResponse200"} } } }, "400": { "description": "Bad Request ", "content": { "application/json": { "schema": {"$ref": "#/components/schemas/httpResponse400"} } } }, "401": {"description": "Unauthorized"}, "403": {"description": "Forbidden ."}, "429": {"description": "Too Many Requests ."}, "500": {"description": "Internal Server Error "} } } }, "/records/RemoveRO": { "post": { "tags": ["Records"], "description": "Remove an existing RO", "requestBody": { "required": true, "content": { "application/json": { "schema": {"$ref": "#/components/schemas/removeRO"} } } }, "responses": { "200": { "description": "OK - Indicates that the request has succeeded.", "content": { "application/json": { "schema": {"$ref": "#/components/schemas/httpResponse200"} } } }, "400": { "description": "Bad Request.", "content": { "application/json": { "schema": {"$ref": "#/components/schemas/httpResponse400"} } } }, "401": {"description": "Unauthorized."}, "403": {"description": "Forbidden"}, "429": {"description": "Too Many Requests ."}, "500": {"description": "Internal Server Error"} } } }, "/records/MoveP": { "post": { "tags": ["Records"], "description": "Move", "requestBody": { "required": true, "content": { "application/json": { "schema": {"$ref": "#/components/schemas/moveP"} } } }, "responses": { "200": { "description": "OK - Indicates that the request has succeeded.", "content": { "application/json": { "schema": {"$ref": "#/components/schemas/httpResponse200"} } } }, "400": { "description": "Bad Request.", "content": { "application/json": { "schema": {"$ref": "#/components/schemas/httpResponse400"} } } }, "401": {"description": "Unauthorized ."}, "403": {"description": "Forbidden "}, "429": {"description": "Too Many Requests ."}, "500": {"description": "Internal Server Error."} } } } }, "components": { "schemas": { "httpResponse200": { "type": "object", "properties": { "results": {"nullable": true, "example": null}, "requestGuid": {"type": "string", "example": "GUI"}, "errors": {"nullable": true, "example": null}, "httpStatusCode": {"type": "integer", "nullable": false, "example": 200} } }, "addUpdateC": { "type": "object", "properties": { "iCR": { "type": "array", "description": "1 to N", "items": { "type": "object", "properties": { "requestGuid": {"type": "string", "example": "GUI", "nullable": false}, "iNumber": {"type": "string", "format": "varchar(10)", "nullable": false}, "eTypeRequest": { "type": "array", "description": "1 to N", "items": { "type": "object", "properties": { "requestGuid": {"type": "string", "example": "16-byte binary value", "nullable": false}, "eTypeECode": {"type": "string", "format": "varchar(3)", "nullable": false, "description": "List"} } } } } } } } }, "moveP": { "type": "object", "properties": { "iMPR": { "type": "object", "properties": { "requestGuid": {"type": "string", "example": "GUID", "nullable": false}, "iNumber": {"type": "string", "format": "varchar(10)", "nullable": false} } } } }, "removeRO": { "type": "object", "properties": { "iRROR": { "type": "object", "properties": { "requestGuid": {"type": "string", "example": "GUID", "nullable": false}, "iNumber": {"type": "string", "format": "varchar(10)", "nullable": false}, "mCRO": {"type": "number", "format": "smallint", "nullable": false, "description": "List"} } } } }, "addUpdateRecords": { "type": "object", "properties": { "iROR": { "type": "object", "properties": { "requestGuid": {"type": "string", "example": "GUID", "nullable": false}, "iNumber": {"type": "string", "format": "varchar(10)", "nullable": false}, "mCRO": {"type": "number", "format": "smallint", "nullable": false, "description": "List"} } }, "iCR": { "type": "array", "description": "1 to N", "items": { "type": "object", "properties": { "requestGuid": {"type": "string", "example": "16-byte binary value", "nullable": false}, "iNumber": {"type": "string", "format": "varchar(10)", "nullable": false}, "eTypeRequest": { "type": "array", "description": "1 to N", "items": { "type": "object", "properties": { "requestGuid": {"type": "string", "example": "16-byte binary value", "nullable": false}, "eTypeECode": {"type": "string", "format": "varchar(3)", "nullable": false, "description": "List"} } } } } } } } }, "updateRO": { "type": "object", "properties": { "iROR": { "type": "object", "properties": { "requestGuid": {"type": "string", "example": "GUID", "nullable": false}, "iNumber": {"type": "string", "format": "varchar(10)", "nullable": false}, "mCRO": {"type": "number", "format": "smallint", "nullable": false, "description": "List"} } } } }, "httpResponse400": { "type": "object", "properties": { "results": {"nullable": true, "example": null}, "requestGuid": {"type": "string", "example": "GUI"}, "errors": { "type": "array", "items": { "type": "object", "properties": { "cTypeCode": {"nullable": true, "example": null}, "cFormula": {"nullable": true, "example": null}, "errorDescription": {"type": "string"} } } }, "httpStatusCode": {"type": "integer", "nullable": false, "example": 400} } }, "httpResponseC400": { "type": "object", "properties": { "results": {"nullable": true, "example": null}, "requestGuid": {"type": "string", "example": "GUI "}, "errors": { "type": "array", "items": { "type": "object", "properties": { "cTypeCode": {"nullable": true, "example": null}, "cFormula": {"nullable": true, "example": null}, "errorDescription": {"type": "string"} } } }, "httpStatusCode": {"type": "integer", "nullable": false, "example": 400} } } } } }
Payload JSON
{ "iRROR": { "requestGuid": "GUID", "iNumber": "12345678910111213", "mCRO": null } }
C# 验证代码
using (StreamReader file = File.OpenText(@"C:\Sample\OpenAPISpecification.json")) using (JsonTextReader reader = new JsonTextReader(file)) { JSchema schema = JSchema.Load(reader); JObject json = JObject.Parse(File.ReadAllText(@"C:\Sample\iRROR.json")); //validate json IList<ValidationError> errors; bool valid = json.IsValid(schema, out errors); new ValidateResponse { Valid = valid, Errors = errors }; }
问题原因与解决方案
核心问题分析
- Schema加载错误:代码直接加载了整个OpenAPI文档作为验证Schema,但OpenAPI根结构不是请求体的Schema,应该提取
components/schemas/removeRO节点作为验证目标。 nullable关键字不兼容:Newtonsoft.Json.Schema默认不识别OpenAPI的nullable扩展字段,这是OpenAPI特有的定义,而非标准JSON Schema规范。- 自定义
format无默认验证:varchar(10)是自定义格式,不属于标准JSON Schema的内置格式(如email、date-time),Newtonsoft.Json.Schema不会自动验证其长度限制。
解决方案
1. 加载正确的目标Schema
修改代码,提取OpenAPI文档中对应的请求体Schema:
using (StreamReader file = File.OpenText(@"C:\Sample\OpenAPISpecification.json")) using (JsonTextReader reader = new JsonTextReader(file)) { // 先加载整个OpenAPI文档 JObject openApiDoc = JObject.Load(reader); // 提取removeRO的Schema节点 JToken targetSchemaToken = openApiDoc.SelectToken("$.components.schemas.removeRO"); JSchema schema = JSchema.Parse(targetSchemaToken.ToString()); JObject json = JObject.Parse(File.ReadAllText(@"C:\Sample\iRROR.json")); IList<ValidationError> errors; bool valid = json.IsValid(schema, out errors); var response = new ValidateResponse { Valid = valid, Errors = errors }; }
2. 处理nullable验证
将OpenAPI的nullable: false转换为标准JSON Schema的required配置,确保字段必填且类型非空:
修改removeRO的Schema,添加required数组:
"removeRO": { "type": "object", "properties": { "iRROR": { "type": "object", "properties": { "requestGuid": {"type": "string", "example": "GUID", "nullable": false}, "iNumber": {"type": "string", "format": "varchar(10)", "nullable": false}, "mCRO": {"type": "number", "format": "smallint", "nullable": false, "description": "List"} }, // 指定必填字段 "required": ["requestGuid", "iNumber", "mCRO"] } }, "required": ["iRROR"] }
3. 实现自定义varchar长度验证
创建自定义验证器处理varchar(N)格式的长度限制:
public class VarcharLengthValidator : IValidator { public bool CanValidate(JSchema schema) { return schema.Type == JSchemaType.String && !string.IsNullOrEmpty(schema.Format) && schema.Format.StartsWith("varchar("); } public void Validate(JToken value, JSchema schema, ValidationContext context, IList<ValidationError> errors) { if (value.Type != JTokenType.String) return;
相关产品推荐
相关产品推荐

