Custom Request Body Validation with GatewayScript in API Connect
Draft!!
When building APIs, robust input validation is critical for security, data integrity, and providing clear error messages to API consumers. While API Connect provides built-in validation through OpenAPI schemas, sometimes you need to explan why the validation failed to the consumers. This article demonstrates how to implement custom request body validation using GatewayScript that validates against a JSON schema and returns detailed error messages for each invalid field.
The Complete API Definition
Here’s a complete OpenAPI 2.0 (Swagger) definition that implements a flexible validation API. The key difference from traditional validation is that both the payload and schema are sent in the request body, making this a reusable validation service:
Note: the schema must be in both the spec and the gatewaysript
swagger: '2.0'
info:
title: Body Validation API
version: 1.0.0
description: >-
API that validates request body against a JSON schema and returns invalid
fields
x-ibm-name: body-validation-api
basePath: /validate
schemes:
- https
consumes:
- application/json
produces:
- application/json
definitions:
ValidationRequest:
type: object
required:
- payload
- schema
properties:
payload:
type: object
description: The data to be validated
schema:
type: object
description: JSON Schema to validate the payload against
properties:
type:
type: string
required:
type: array
items:
type: string
properties:
type: object
additionalProperties:
type: boolean
additionalProperties: false
ValidationSuccess:
type: object
properties:
status:
type: string
example: success
message:
type: string
example: Validation passed
ValidationError:
type: object
properties:
status:
type: string
example: error
message:
type: string
example: Validation failed
errors:
type: array
items:
type: object
properties:
field:
type: string
description: The field that failed validation
message:
type: string
description: Description of the validation error
value:
type: string
description: The actual value that failed validation
paths:
/user:
post:
summary: Validate user data
description: Validates user data against a provided schema
parameters:
- name: body
in: body
required: true
schema:
$ref: '#/definitions/ValidationRequest'
responses:
'200':
description: Validation successful
schema:
$ref: '#/definitions/ValidationSuccess'
'400':
description: Validation failed
schema:
$ref: '#/definitions/ValidationError'
x-ibm-configuration:
enforced: true
testable: true
phase: realized
cors:
enabled: true
gateway: datapower-api-gateway
type: rest
assembly:
execute:
- parse:
version: 2.0.0
title: Parse JSON Body
parse-settings-reference:
default: apic-default-parsesettings
- gatewayscript:
version: 2.0.0
title: Handle Validation
source: |
// Get the parsed request body (already parsed by the parse policy)
var requestBody = context.get('request.body');
// Extract payload and schema from request
var payload = requestBody.payload;
var schema = requestBody.schema;
// Validate that both payload and schema are present
if (!payload) {
context.set('message.status.code', 400);
context.set('message.body', {
status: 'error',
message: 'Missing payload in request body',
errors: [{
field: 'payload',
message: 'payload field is required',
value: 'undefined'
}]
});
return;
}
if (!schema) {
context.set('message.status.code', 400);
context.set('message.body', {
status: 'error',
message: 'Missing schema in request body',
errors: [{
field: 'schema',
message: 'schema field is required',
value: 'undefined'
}]
});
return;
}
// Validation function
function validateSchema(data, schema, path) {
var errors = [];
path = path || '';
// Check required fields
if (schema.required) {
for (var i = 0; i < schema.required.length; i++) {
var requiredField = schema.required[i];
if (data[requiredField] === undefined || data[requiredField] === null) {
errors.push({
field: path + requiredField,
message: 'Required field is missing',
value: 'undefined'
});
}
}
}
// Check properties
if (schema.properties) {
for (var prop in data) {
if (data.hasOwnProperty(prop)) {
var propSchema = schema.properties[prop];
var value = data[prop];
var fieldPath = path + prop;
if (!propSchema && schema.additionalProperties === false) {
errors.push({
field: fieldPath,
message: 'Additional property not allowed',
value: String(value)
});
continue;
}
if (propSchema) {
// Type validation
var actualType = Array.isArray(value) ? 'array' : typeof value;
if (actualType === 'object' && value === null) {
actualType = 'null';
}
if (propSchema.type && actualType !== propSchema.type) {
errors.push({
field: fieldPath,
message: 'Invalid type. Expected ' + propSchema.type + ' but got ' + actualType,
value: String(value)
});
continue;
}
// String validations
if (propSchema.type === 'string' && typeof value === 'string') {
if (propSchema.minLength && value.length < propSchema.minLength) {
errors.push({
field: fieldPath,
message: 'String length must be at least ' + propSchema.minLength + ' characters',
value: value
});
}
if (propSchema.maxLength && value.length > propSchema.maxLength) {
errors.push({
field: fieldPath,
message: 'String length must not exceed ' + propSchema.maxLength + ' characters',
value: value
});
}
if (propSchema.pattern) {
var regex = new RegExp(propSchema.pattern);
if (!regex.test(value)) {
errors.push({
field: fieldPath,
message: 'String does not match required pattern',
value: value
});
}
}
}
// Number/Integer validations
if ((propSchema.type === 'number' || propSchema.type === 'integer') && typeof value === 'number') {
if (propSchema.minimum !== undefined && value < propSchema.minimum) {
errors.push({
field: fieldPath,
message: 'Value must be at least ' + propSchema.minimum,
value: String(value)
});
}
if (propSchema.maximum !== undefined && value > propSchema.maximum) {
errors.push({
field: fieldPath,
message: 'Value must not exceed ' + propSchema.maximum,
value: String(value)
});
}
}
}
}
}
}
return errors;
}
// Perform validation using the provided schema
var validationErrors = validateSchema(payload, schema, '');
// Set response based on validation results
if (validationErrors.length > 0) {
context.set('message.status.code', 400);
context.set('message.body', {
status: 'error',
message: 'Validation failed',
errors: validationErrors
});
} else {
context.set('message.status.code', 200);
context.set('message.body', {
status: 'success',
message: 'Validation passed',
data: payload
});
}
activity-log:
enabled: true
success-content: activity
error-content: payload
Understanding the Implementation
1. Request Structure
This API uses a flexible approach where both the payload and schema are sent in the request body:
Request Body Structure (ValidationRequest):
{
"payload": {
// The data to be validated
},
"schema": {
// JSON Schema definition
"type": "object",
"required": ["field1", "field2"],
"properties": {
// Property definitions
}
}
}
Key Benefits:
- Dynamic validation: No need to redeploy the API for different schemas
- Reusable service: One API can validate any payload against any schema
- Flexible: Supports different validation requirements without code changes
- Testable: Easy to test different schemas and payloads
Example Requests and Responses
Valid Request
Request:
curl -X POST https://api.example.com/validate/user \
-H "Content-Type: application/json" \
-d '{
"payload": {
"name": "John Doe",
"email": "john.doe@example.com",
"age": 30
},
"schema": {
"type": "object",
"required": ["name", "email", "age"],
"properties": {
"name": {
"type": "string",
"minLength": 2,
"maxLength": 50,
"pattern": "^[a-zA-Z\\s]+$"
},
"email": {
"type": "string",
"pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
},
"age": {
"type": "number",
"minimum": 18,
"maximum": 120
}
},
"additionalProperties": false
}
}'
Response (200 OK):
{
"status": "success",
"message": "Validation passed",
"data": {
"name": "John Doe",
"email": "john.doe@example.com",
"age": 30
}
}
Invalid Request - Missing Required Field
Request:
curl -X POST https://api.example.com/validate/user \
-H "Content-Type: application/json" \
-d '{
"payload": {
"name": "John Doe",
"email": "john.doe@example.com"
},
"schema": {
"type": "object",
"required": ["name", "email", "age"],
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
"age": {"type": "number"}
}
}
}'
Response (400 Bad Request):
{
"status": "error",
"message": "Validation failed",
"errors": [
{
"field": "age",
"message": "Required field is missing",
"value": "undefined"
}
]
}
Invalid Request - Multiple Validation Errors
Request:
curl -X POST https://api.example.com/validate/user \
-H "Content-Type: application/json" \
-d '{
"payload": {
"name": "J",
"email": "invalid-email",
"age": 15,
"extraField": "not allowed"
},
"schema": {
"type": "object",
"required": ["name", "email", "age"],
"properties": {
"name": {
"type": "string",
"minLength": 2
},
"email": {
"type": "string",
"pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
},
"age": {
"type": "number",
"minimum": 18
}
},
"additionalProperties": false
}
}'
Response (400 Bad Request):
{
"status": "error",
"message": "Validation failed",
"errors": [
{
"field": "name",
"message": "String length must be at least 2 characters",
"value": "J"
},
{
"field": "email",
"message": "String does not match required pattern",
"value": "invalid-email"
},
{
"field": "age",
"message": "Value must be at least 18",
"value": "15"
},
{
"field": "extraField",
"message": "Additional property not allowed",
"value": "not allowed"
}
]
}
Invalid Request - Wrong Data Type
Request:
curl -X POST https://api.example.com/validate/user \
-H "Content-Type: application/json" \
-d '{
"payload": {
"name": "John Doe",
"email": "john.doe@example.com",
"age": "thirty"
},
"schema": {
"type": "object",
"required": ["name", "email", "age"],
"properties": {
"name": {"type": "string"},
"email": {"type": "string"},
"age": {"type": "number"}
}
}
}'
Response (400 Bad Request):
{
"status": "error",
"message": "Validation failed",
"errors": [
{
"field": "age",
"message": "Invalid type. Expected number but got string",
"value": "thirty"
}
]
}
