API Contract Testing with JSON Schema in Playwright
API tests often check only the happy path: “Does the endpoint return 200?” But what happens when the response structure changes? A field gets renamed, a required property becomes optional, or a string becomes a number. Your tests pass, but your application breaks in production.
This guide shows you how to implement contract testing with JSON Schema in Playwright—a technique that validates not just status codes, but the entire structure, types, and constraints of your API responses.
The Problem: Testing Beyond Status Codes
Section titled “The Problem: Testing Beyond Status Codes”Consider this typical API test:
test('get user profile', async ({ request }) => { const response = await request.get('/api/users/123');
expect(response.status()).toBe(200);
const data = await response.json(); expect(data.email).toBe('user@example.com');});What this test misses:
- ✗ Type changes (string → number)
- ✗ Missing required fields
- ✗ Extra unexpected fields
- ✗ Null values in non-nullable fields
- ✗ Invalid formats (email, date, URL)
- ✗ Array items with inconsistent structure
- ✗ Nested object validation
Example of a breaking change this test would miss:
// Before (working){ "email": "user@example.com", "age": 30, "created_at": "2024-01-15T10:30:00Z"}
// After (BROKEN but test passes!){ "email": "user@example.com", "age": "30", // ← Now string instead of number "created": "2024-01-15" // ← Field renamed, wrong format}Your test checks data.email and passes, but your application crashes because it expects age to be a number and can’t find created_at.
The Solution: JSON Schema Validation
Section titled “The Solution: JSON Schema Validation”JSON Schema is a vocabulary that allows you to annotate and validate JSON documents. It defines:
- Data types (string, number, boolean, object, array, null)
- Required vs optional fields
- Value constraints (min/max, patterns, formats)
- Structure validation (nested objects, array items)
- Custom rules (enums, conditionals, dependencies)
With JSON Schema, you define what a valid API response looks like once, and validate every test against that contract.
Quick Start: Basic Schema Validation
Section titled “Quick Start: Basic Schema Validation”Step 1: Install AJV (JSON Schema Validator)
Section titled “Step 1: Install AJV (JSON Schema Validator)”npm install --save-dev ajv ajv-formatsWhy AJV?
- Fast (compiles schemas for performance)
- Full JSON Schema support (draft 7, 2019-09, 2020-12)
- Format validation (email, date-time, URL, etc.)
- Custom keywords support
- Excellent error messages
Step 2: Create Your First Schema
Section titled “Step 2: Create Your First Schema”Create schemas/user-profile.schema.json:
{ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "id": { "type": "string", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" }, "email": { "type": "string", "format": "email" }, "name": { "type": "string", "minLength": 1, "maxLength": 100 }, "age": { "type": "integer", "minimum": 0, "maximum": 150 }, "created_at": { "type": "string", "format": "date-time" }, "is_active": { "type": "boolean" } }, "required": ["id", "email", "name", "created_at", "is_active"], "additionalProperties": false}What this schema defines:
- ✅
idmust be a UUID string - ✅
emailmust be a valid email format - ✅
namemust be 1-100 characters - ✅
ageis optional but must be 0-150 if present - ✅
created_atmust be ISO 8601 date-time - ✅
is_activemust be boolean - ✅ No extra fields allowed (
additionalProperties: false)
Step 3: Create Validation Utility
Section titled “Step 3: Create Validation Utility”Create utilities/schema-validator.js:
const Ajv = require('ajv');const addFormats = require('ajv-formats');
// Initialize AJV with formats supportconst ajv = new Ajv({ allErrors: true, verbose: true });addFormats(ajv);
/** * Validate API response against JSON schema * * @param {Object} responseBody - The API response to validate * @param {Object} schema - The JSON schema to validate against * @throws {Error} If validation fails */function validateSchema(responseBody, schema) { const validate = ajv.compile(schema); const valid = validate(responseBody);
if (!valid) { const errors = formatValidationErrors(validate.errors); console.error('❌ Schema validation failed:'); console.error(errors); throw new Error(`Schema validation failed:\n${errors}`); }
console.log('✅ Response validated successfully against schema');}
/** * Format AJV errors into readable messages */function formatValidationErrors(errors) { return errors.map(error => { const path = error.instancePath || 'root'; return ` • ${path}: ${error.message}`; }).join('\n');}
module.exports = { validateSchema };Step 4: Use in Tests
Section titled “Step 4: Use in Tests”const { test, expect } = require('@playwright/test');const { validateSchema } = require('../utilities/schema-validator');const userSchema = require('../schemas/user-profile.schema.json');
test('get user profile with schema validation', async ({ request }) => { const response = await request.get('/api/users/123');
// Basic assertion expect(response.status()).toBe(200);
// Schema validation - validates EVERYTHING const data = await response.json(); validateSchema(data, userSchema);
// Now we can safely use the data // We KNOW it has the correct structure and types expect(data.email).toContain('@');});What changed:
- ✅ One line validates entire response structure
- ✅ Catches type changes, missing fields, format issues
- ✅ Clear error messages pinpoint exact problems
- ✅ Self-documenting (schema describes API contract)
Real-World Example: Complex Schema
Section titled “Real-World Example: Complex Schema”Let’s validate a realistic API response with nested objects and arrays.
API Response: E-commerce Order
Section titled “API Response: E-commerce Order”{ "order_id": "ORD-2024-001", "customer": { "id": "CUST-123", "email": "customer@example.com", "name": "John Doe" }, "items": [ { "product_id": "PROD-456", "name": "Blue T-Shirt", "quantity": 2, "price": 29.99, "discount": 0.1 }, { "product_id": "PROD-789", "name": "Running Shoes", "quantity": 1, "price": 89.99, "discount": null } ], "shipping": { "method": "express", "address": { "street": "123 Main St", "city": "New York", "postal_code": "10001", "country": "US" }, "cost": 15.00 }, "totals": { "subtotal": 149.97, "shipping": 15.00, "tax": 14.50, "total": 179.47 }, "status": "confirmed", "created_at": "2024-01-15T14:30:00Z"}Schema Definition
Section titled “Schema Definition”Create schemas/order.schema.json:
{ "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "properties": { "order_id": { "type": "string", "pattern": "^ORD-\\d{4}-\\d{3}$" }, "customer": { "type": "object", "properties": { "id": { "type": "string", "pattern": "^CUST-\\d+$" }, "email": { "type": "string", "format": "email" }, "name": { "type": "string", "minLength": 1 } }, "required": ["id", "email", "name"], "additionalProperties": false }, "items": { "type": "array", "minItems": 1, "items": { "type": "object", "properties": { "product_id": { "type": "string", "pattern": "^PROD-\\d+$" }, "name": { "type": "string", "minLength": 1 }, "quantity": { "type": "integer", "minimum": 1 }, "price": { "type": "number", "minimum": 0 }, "discount": { "type": ["number", "null"], "minimum": 0, "maximum": 1 } }, "required": ["product_id", "name", "quantity", "price"], "additionalProperties": false } }, "shipping": { "type": "object", "properties": { "method": { "type": "string", "enum": ["standard", "express", "overnight"] }, "address": { "type": "object", "properties": { "street": { "type": "string" }, "city": { "type": "string" }, "postal_code": { "type": "string" }, "country": { "type": "string", "pattern": "^[A-Z]{2}$" } }, "required": ["street", "city", "postal_code", "country"] }, "cost": { "type": "number", "minimum": 0 } }, "required": ["method", "address", "cost"] }, "totals": { "type": "object", "properties": { "subtotal": { "type": "number", "minimum": 0 }, "shipping": { "type": "number", "minimum": 0 }, "tax": { "type": "number", "minimum": 0 }, "total": { "type": "number", "minimum": 0 } }, "required": ["subtotal", "shipping", "tax", "total"] }, "status": { "type": "string", "enum": ["pending", "confirmed", "shipped", "delivered", "cancelled"] }, "created_at": { "type": "string", "format": "date-time" } }, "required": [ "order_id", "customer", "items", "shipping", "totals", "status", "created_at" ], "additionalProperties": false}This schema validates:
- ✅ Order ID format (
ORD-2024-001) - ✅ Customer object structure with email validation
- ✅ At least one item in order
- ✅ Each item has correct product ID format
- ✅ Prices are non-negative numbers
- ✅ Discounts are 0-1 or null
- ✅ Shipping method is one of three allowed values
- ✅ Country code is 2-letter uppercase
- ✅ Status is valid enum value
- ✅ Created timestamp is ISO 8601 format
- ✅ All required fields present
- ✅ No unexpected extra fields
Test Implementation
Section titled “Test Implementation”const { test, expect } = require('@playwright/test');const { validateSchema } = require('../utilities/schema-validator');const orderSchema = require('../schemas/order.schema.json');
test.describe('Order API', () => { test('create order returns valid structure', async ({ request }) => { const response = await request.post('/api/orders', { data: { customer_id: 'CUST-123', items: [ { product_id: 'PROD-456', quantity: 2 }, { product_id: 'PROD-789', quantity: 1 } ], shipping_method: 'express' } });
expect(response.status()).toBe(201);
const order = await response.json();
// Validate entire order structure validateSchema(order, orderSchema);
// Additional business logic assertions expect(order.status).toBe('confirmed'); expect(order.totals.total).toBeGreaterThan(0); });
test('get order by id returns valid structure', async ({ request }) => { const response = await request.get('/api/orders/ORD-2024-001');
expect(response.status()).toBe(200);
const order = await response.json(); validateSchema(order, orderSchema); });
test('list orders returns array of valid orders', async ({ request }) => { const response = await request.get('/api/orders');
expect(response.status()).toBe(200);
const orders = await response.json();
// Validate array expect(Array.isArray(orders)).toBe(true);
// Validate each order orders.forEach(order => { validateSchema(order, orderSchema); }); });});Advanced Patterns
Section titled “Advanced Patterns”Pattern 1: Schema Organization by Service
Section titled “Pattern 1: Schema Organization by Service”Organize schemas to match your API structure:
schemas/├── config/│ ├── materials.schema.json│ ├── areas.schema.json│ └── equipment.schema.json├── analytics/│ ├── insights.schema.json│ ├── trends.schema.json│ └── metrics.schema.json├── orders/│ ├── order.schema.json│ ├── order-list.schema.json│ └── order-summary.schema.json└── common/ ├── error-response.schema.json ├── pagination.schema.json └── timestamp.schema.jsonPattern 2: Schema Reuse with $ref
Section titled “Pattern 2: Schema Reuse with $ref”Avoid duplication by referencing common schemas:
{ "$id": "address.schema.json", "type": "object", "properties": { "street": { "type": "string" }, "city": { "type": "string" }, "postal_code": { "type": "string" }, "country": { "type": "string", "pattern": "^[A-Z]{2}$" } }, "required": ["street", "city", "postal_code", "country"]}
// schemas/common/customer.schema.json{ "$id": "customer.schema.json", "type": "object", "properties": { "id": { "type": "string" }, "email": { "type": "string", "format": "email" }, "name": { "type": "string" }, "billing_address": { "$ref": "address.schema.json" }, "shipping_address": { "$ref": "address.schema.json" } }, "required": ["id", "email", "name"]}Load schemas with references:
const Ajv = require('ajv');const ajv = new Ajv({ allErrors: true });
// Load referenced schemasconst addressSchema = require('./schemas/common/address.schema.json');const customerSchema = require('./schemas/common/customer.schema.json');
ajv.addSchema(addressSchema);ajv.addSchema(customerSchema);
// Now validateconst validate = ajv.compile(customerSchema);const valid = validate(customerData);Pattern 3: Dynamic Schema Selection
Section titled “Pattern 3: Dynamic Schema Selection”Choose schema based on endpoint or test context:
const schemas = { 'materials': require('./schemas/config/materials.schema.json'), 'areas': require('./schemas/config/areas.schema.json'), 'equipment': require('./schemas/config/equipment.schema.json'),};
function validateEndpoint(endpointName, responseBody) { const schema = schemas[endpointName];
if (!schema) { throw new Error(`No schema found for endpoint: ${endpointName}`); }
validateSchema(responseBody, schema);}
// Usagetest('get materials', async ({ request }) => { const response = await request.get('/api/config/materials'); const data = await response.json();
validateEndpoint('materials', data);});Pattern 4: Schema Validation with Custom Error Reporting
Section titled “Pattern 4: Schema Validation with Custom Error Reporting”Enhanced validator with detailed error reporting:
const Ajv = require('ajv');const addFormats = require('ajv-formats');
class SchemaValidator { constructor() { this.ajv = new Ajv({ allErrors: true, verbose: true, strict: false }); addFormats(this.ajv); this.compiledSchemas = new Map(); }
/** * Validate response with detailed error reporting */ validate(responseBody, schema, context = {}) { const schemaKey = JSON.stringify(schema);
// Cache compiled schemas for performance if (!this.compiledSchemas.has(schemaKey)) { this.compiledSchemas.set(schemaKey, this.ajv.compile(schema)); }
const validate = this.compiledSchemas.get(schemaKey); const valid = validate(responseBody);
if (!valid) { this.throwDetailedError(validate.errors, responseBody, context); }
console.log(`✅ Schema validation passed ${context.endpoint || ''}`); return true; }
/** * Format validation errors with context */ throwDetailedError(errors, responseBody, context) { const errorReport = [ '❌ Schema Validation Failed', '', ...(context.endpoint ? [`Endpoint: ${context.endpoint}`] : []), ...(context.method ? [`Method: ${context.method}`] : []), '', 'Validation Errors:', ...errors.map((err, index) => { const path = err.instancePath || '/'; const value = this.getValueAtPath(responseBody, path);
return [ ` ${index + 1}. ${err.message}`, ` Path: ${path}`, ` Expected: ${JSON.stringify(err.params)}`, ` Received: ${JSON.stringify(value)}` ].join('\n'); }), '' ].join('\n');
throw new Error(errorReport); }
/** * Get value from response at JSON path */ getValueAtPath(obj, path) { if (!path || path === '/') return obj;
const parts = path.split('/').filter(Boolean); let current = obj;
for (const part of parts) { if (current === undefined || current === null) break; current = current[part]; }
return current; }}
// Export singletonconst validator = new SchemaValidator();
function validateSchema(responseBody, schema, context) { return validator.validate(responseBody, schema, context);}
module.exports = { validateSchema, SchemaValidator };Enhanced error output:
❌ Schema Validation Failed
Endpoint: /api/orders/123Method: GET
Validation Errors: 1. must have required property 'created_at' Path: / Expected: {"missingProperty":"created_at"} Received: {"order_id":"ORD-123","status":"confirmed"}
2. must be string Path: /status Expected: {"type":"string"} Received: 123
3. must be one of: pending, confirmed, shipped Path: /status Expected: {"allowedValues":["pending","confirmed","shipped"]} Received: 123Pattern 5: Conditional Validation Based on Response
Section titled “Pattern 5: Conditional Validation Based on Response”Different schemas for different response types:
function validateOrderResponse(responseBody) { // Check status to determine schema if (responseBody.status === 'pending') { validateSchema(responseBody, pendingOrderSchema); } else if (responseBody.status === 'shipped') { validateSchema(responseBody, shippedOrderSchema); // Includes tracking_number } else if (responseBody.status === 'cancelled') { validateSchema(responseBody, cancelledOrderSchema); // Includes cancellation_reason } else { validateSchema(responseBody, baseOrderSchema); }}Or use JSON Schema’s conditional validation:
{ "type": "object", "properties": { "status": { "type": "string" } }, "if": { "properties": { "status": { "const": "shipped" } } }, "then": { "required": ["tracking_number"], "properties": { "tracking_number": { "type": "string" } } }}Pattern 6: Schema Generation from API Response
Section titled “Pattern 6: Schema Generation from API Response”For existing APIs without documentation, generate schemas from responses:
const Ajv = require('ajv');const standaloneCode = require('ajv/dist/standalone').default;
/** * Generate JSON Schema from API response * (Simplified - use libraries like json-schema-generator for production) */function generateSchemaFromResponse(response) { function inferType(value) { if (value === null) return { type: 'null' }; if (Array.isArray(value)) { return { type: 'array', items: value.length > 0 ? inferType(value[0]) : {} }; } if (typeof value === 'object') { const properties = {}; const required = [];
for (const [key, val] of Object.entries(value)) { properties[key] = inferType(val); required.push(key); }
return { type: 'object', properties, required }; }
return { type: typeof value }; }
return { $schema: 'http://json-schema.org/draft-07/schema#', ...inferType(response) };}
// Usage - generate schema from actual responsetest('generate schema for new endpoint', async ({ request }) => { const response = await request.get('/api/new-endpoint'); const data = await response.json();
const schema = generateSchemaFromResponse(data); console.log('Generated schema:', JSON.stringify(schema, null, 2));
// Save for future use // fs.writeFileSync('schemas/new-endpoint.schema.json', JSON.stringify(schema, null, 2));});Testing Schema Changes
Section titled “Testing Schema Changes”Negative Testing: Validate Schema Catches Errors
Section titled “Negative Testing: Validate Schema Catches Errors”Test that your schemas actually catch problems:
test.describe('Schema Validation - Negative Tests', () => { test('should reject missing required field', () => { const invalidData = { email: 'user@example.com', // Missing 'name' required field };
expect(() => { validateSchema(invalidData, userSchema); }).toThrow(/must have required property 'name'/); });
test('should reject wrong data type', () => { const invalidData = { name: 'John Doe', email: 'user@example.com', age: '30' // Should be number, not string };
expect(() => { validateSchema(invalidData, userSchema); }).toThrow(/must be number/); });
test('should reject invalid email format', () => { const invalidData = { name: 'John Doe', email: 'not-an-email', age: 30 };
expect(() => { validateSchema(invalidData, userSchema); }).toThrow(/must match format "email"/); });
test('should reject extra fields when additionalProperties: false', () => { const invalidData = { name: 'John Doe', email: 'user@example.com', age: 30, unexpectedField: 'should not be here' };
expect(() => { validateSchema(invalidData, userSchema); }).toThrow(/must NOT have additional properties/); });});Integration with Test Framework
Section titled “Integration with Test Framework”Playwright Test Fixture for Schema Validation
Section titled “Playwright Test Fixture for Schema Validation”const { test as base } = require('@playwright/test');const { validateSchema } = require('../utilities/schema-validator');
const test = base.extend({ /** * Fixture that provides schema validation */ schemaValidator: async ({}, use) => { const validator = { validate: (data, schema, context) => { validateSchema(data, schema, context); },
validateResponse: async (response, schema) => { const data = await response.json(); const context = { endpoint: new URL(response.url()).pathname, method: 'GET', status: response.status() }; validateSchema(data, schema, context); return data; } };
await use(validator); }});
module.exports = { test };Usage:
const { test } = require('./fixtures/schema-fixtures');const { expect } = require('@playwright/test');const userSchema = require('./schemas/user.schema.json');
test('get user with fixture', async ({ request, schemaValidator }) => { const response = await request.get('/api/users/123');
expect(response.status()).toBe(200);
// Validate and get data in one call const user = await schemaValidator.validateResponse(response, userSchema);
// Now use the validated data expect(user.email).toContain('@');});Best Practices
Section titled “Best Practices”1. Schema Versioning
Section titled “1. Schema Versioning”Track schema versions alongside API versions:
schemas/├── v1/│ ├── user.schema.json│ └── order.schema.json├── v2/│ ├── user.schema.json│ └── order.schema.json└── latest/ -> v2/function getSchema(entity, version = 'latest') { return require(`./schemas/${version}/${entity}.schema.json`);}
test('test against v1 API', async ({ request }) => { const response = await request.get('/v1/api/users/123'); const data = await response.json(); validateSchema(data, getSchema('user', 'v1'));});2. Schema Documentation
Section titled “2. Schema Documentation”Use descriptions in schemas:
{ "type": "object", "description": "User profile returned by /api/users/:id endpoint", "properties": { "email": { "type": "string", "format": "email", "description": "User's primary email address (unique)" }, "age": { "type": "integer", "minimum": 0, "maximum": 150, "description": "User's age in years (optional)" } }}3. Strict vs. Loose Validation
Section titled “3. Strict vs. Loose Validation”Choose appropriate strictness:
// Strict: Fail on ANY extra fieldsconst strictSchema = { type: 'object', properties: { /* ... */ }, additionalProperties: false // ← Strict};
// Loose: Allow extra fields (forward compatibility)const looseSchema = { type: 'object', properties: { /* ... */ }, additionalProperties: true // ← Loose};When to use strict:
- Internal APIs you control
- Critical data that must match exactly
- Security-sensitive responses
When to use loose:
- Third-party APIs
- APIs that may add fields
- Backward compatibility requirements
4. Performance Optimization
Section titled “4. Performance Optimization”// Pre-compile schemas at startupconst Ajv = require('ajv');const ajv = new Ajv();
const compiledSchemas = { user: ajv.compile(require('./schemas/user.schema.json')), order: ajv.compile(require('./schemas/order.schema.json')), product: ajv.compile(require('./schemas/product.schema.json'))};
function validateFast(data, schemaName) { const validate = compiledSchemas[schemaName]; if (!validate(data)) { throw new Error(ajv.errorsText(validate.errors)); }}
// Much faster than compiling on each validationReal-World Example: Complete Test Suite
Section titled “Real-World Example: Complete Test Suite”Here’s a production-ready test suite with schema validation:
const { test, expect } = require('@playwright/test');const { validateSchema } = require('../utilities/schema-validator');
// Load all schemasconst schemas = { materialGroups: require('../schemas/config/material-groups.schema.json'), materials: require('../schemas/config/materials.schema.json'), areas: require('../schemas/config/areas.schema.json'), equipment: require('../schemas/config/equipment.schema.json')};
let authToken;
test.beforeAll(async ({ request }) => { // Get authentication token (see Token Management guide) const loginResponse = await request.post('/auth/login', { data: { email: process.env.TEST_USER_EMAIL, password: process.env.TEST_USER_PASSWORD } });
const body = await loginResponse.json(); authToken = `Bearer ${body.token}`;});
test.describe('Config Service API - Contract Tests', () => {
test('[TC-001] GET /material-groups returns valid structure', async ({ request }) => { const response = await request.get('/api/config/material-groups', { headers: { Authorization: authToken } });
expect(response.status()).toBe(200);
const data = await response.json(); validateSchema(data, schemas.materialGroups);
// Additional business logic assertions expect(Array.isArray(data)).toBe(true); expect(data.length).toBeGreaterThan(0); });
test('[TC-002] GET /materials returns valid structure', async ({ request }) => { const response = await request.get('/api/config/materials', { headers: { Authorization: authToken } });
expect(response.status()).toBe(200);
const data = await response.json(); validateSchema(data, schemas.materials);
// Verify each material has required properties data.forEach(material => { expect(material.id).toBeTruthy(); expect(material.name).toBeTruthy(); expect(typeof material.is_ore).toBe('boolean'); }); });
test('[TC-003] GET /areas returns valid structure', async ({ request }) => { const response = await request.get('/api/config/areas', { headers: { Authorization: authToken } });
expect(response.status()).toBe(200);
const data = await response.json(); validateSchema(data, schemas.areas); });
test('[TC-004] GET /equipment returns valid structure', async ({ request }) => { const response = await request.get('/api/config/equipment', { headers: { Authorization: authToken } });
expect(response.status()).toBe(200);
const data = await response.json(); validateSchema(data, schemas.equipment); });
test('[TC-005] POST /materials creates material with valid response', async ({ request }) => { const newMaterial = { name: `Test Material ${Date.now()}`, material_group_id: 'GROUP-001', is_ore: true, description: 'Test material for automation' };
const response = await request.post('/api/config/materials', { headers: { Authorization: authToken }, data: newMaterial });
expect(response.status()).toBe(201);
const created = await response.json();
// Validate response structure validateSchema(created, schemas.materials.items);
// Verify created material matches input expect(created.name).toBe(newMaterial.name); expect(created.is_ore).toBe(newMaterial.is_ore); expect(created.id).toBeTruthy(); // Server generates ID });});Conclusion
Section titled “Conclusion”JSON Schema validation transforms API testing from checking status codes to enforcing complete API contracts. By validating response structure, data types, formats, and constraints, you catch breaking changes before they reach production.
Key Benefits:
- ✅ Early detection of API contract violations
- ✅ Self-documenting tests and APIs
- ✅ Type safety without TypeScript
- ✅ Regression prevention - schema catches subtle changes
- ✅ Better error messages - pinpoints exact validation failures
- ✅ Confidence - know your data structure is correct
Implementation Checklist:
- Install AJV and ajv-formats
- Create schemas for your key endpoints
- Build a validation utility
- Add validation to existing tests
- Write negative tests for schema validation
- Organize schemas by service/version
- Consider pre-compiling schemas for performance
Next Steps:
- Start with your most critical API endpoints
- Generate schemas from actual responses
- Add schema validation to CI/CD pipeline
- Version schemas alongside API versions
- Create shared schemas for common structures
With JSON Schema validation in place, your API tests become robust contracts that protect against breaking changes and ensure data integrity across your entire application. 🛡️