Search code examples
json-schema-validator

Validate request body against dynamic OpenAPI specification with json-schema-validator


I need to build an API to validate the request body against the registered schema for the respective type & subType.

API Contract:

{
    "id": "<any-uuid>",
    "type": "<some-type>",
    "subType": "<some-sub-type>",
    "data": {
        
    }
}

Here, OpenAPI schema will be fetched based on the type and subType and then need to validate the data element against the respective OpenAPI schema.

Wrote the below snippet:

Map<String, Object> data = //get the data object from API request body;
JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(VersionFlag.V7);
ObjectMapper objectMapper = new ObjectMapper();
JsonNode node = objectMapper.convertValue(data, JsonNode.class);
String schemaJson = // fetch the registered schema for type and subtype
JsonSchema schema = jsonSchemaFactory.getSchema(schemaJson);
Set<ValidationMessage> errors = schema.validate(node);

// Throw exception when errors present in the Json Payload
if (errors.size() > 0) {
    // throw the exception with errors
}

This code is working, when the schema don't have:

  1. Few elements such as openapi, paths, info, components.
  2. When one object not referring other.
  • API Schema in our database as follows:
{
  "openapi": "3.0.0",
  "paths": {},
  "info": {
    "title": "Patient Info API",
    "version": "v0.1.0"
  },
  "components": {
    "schemas": {
      "Data": {
        "type": "object",
        "required": [
          "action",
          "patient"
        ],
        "properties": {
          "action": {
            "type": "string",
            "enum": [
              "ADMIT",
              "DISCHARGE",
              "TRANSFER"
            ]
          },
          "patient": {
            "$ref": "#/components/schemas/Patient"
          }
        }
      },
      "Patient": {
        "type": "object",
        "required": [
          "firstName",
          "lastName"
        ],
        "properties": {
          "firstName": {
            "type": "string"
          },
          "lastName": {
            "type": "string"
          }
        }
      }
    }
  }
}
  • The data element in the API request body looks like this.
{
    "action": "ADMIT",
    "patient": {
        "firstName": "John",
        "lastName": "Doe"
    }
}

Can json-schema-validator help to achieve this?


Solution

  • For simplicity, wrote all the logic inside the Spring Controller itself.

    package com.schema.validator.controller;
    
    import com.fasterxml.jackson.databind.JsonNode;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import com.fasterxml.jackson.databind.node.ObjectNode;
    import com.google.common.io.Files;
    import com.networknt.schema.*;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.core.io.Resource;
    import org.springframework.http.ResponseEntity;
    import org.springframework.util.CollectionUtils;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.io.IOException;
    import java.nio.charset.Charset;
    import java.util.Collections;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    import java.util.stream.Collectors;
    
    /**
     * A REST API to validate the request body against Open API Schema
     *
     */
    @RestController
    @RequestMapping("schema-validate")
    public class SchemaValidatorController {
    
        private static final ObjectMapper objectMapper = new ObjectMapper();
    
        private static final JsonSchemaFactory jsonSchemaFactory =
                JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V6);
    
        @Value("classpath:schema/json-schema.json")
        private Resource schemaResource;
    
        @PostMapping
        public ResponseEntity<List<String>> validateSchema(@RequestBody Map<String, Object> jsonRequest) throws IOException {
    
            String schemaJson = Files.asCharSource(schemaResource.getFile(), Charset.defaultCharset()).read();
    
            final JsonNode requestJsonNode = objectMapper.valueToTree(jsonRequest);
            JsonNode componentNode = objectMapper.readTree(schemaJson).get("components");
            // Get first name in the JsonNode
            JsonNode schemaNode = componentNode.get("schemas").elements().next();
    
            ((ObjectNode) schemaNode).set("components", componentNode);
            SchemaValidatorsConfig config = new SchemaValidatorsConfig();
            config.setTypeLoose(false);
            config.setHandleNullableField(true);
            JsonSchema jsonSchema = jsonSchemaFactory.getSchema(schemaNode, config);
    
            Set<ValidationMessage> errors =
                    jsonSchema.validate(requestJsonNode, requestJsonNode, "data");
    
            if (CollectionUtils.isEmpty(errors)) {
                return ResponseEntity.ok(Collections.emptyList());
            }
    
            return ResponseEntity.ok(errors.stream()
                    .map(ValidationMessage::getMessage)
                    .collect(Collectors.toList()));
        }
    
    }
    

    Here, json-schema.json created under resources/schema folder.

    Limitation: The root element in Open API schema should be declared as a first child of schemas in Open API spec.