Workflows
3 min

API Response Validation

Common patterns for validating API responses

...
apivalidationtestingbest-practices

API Response Validation Patterns

1. Status Code Validation

Common HTTP Status Codes:

// Success codes
200 OK              // Successful GET, PUT, PATCH
201 Created         // Successful POST (resource created)
204 No Content      // Successful DELETE or update with no body
 
// Client error codes
400 Bad Request     // Invalid request data
401 Unauthorized    // Missing or invalid authentication
403 Forbidden       // Insufficient permissions
404 Not Found       // Resource doesn't exist
409 Conflict        // Duplicate or conflicting state
422 Unprocessable   // Validation errors
 
// Server error codes
500 Internal Error  // Server-side error
503 Service Unavailable // Temporary unavailability

REST Assured (Java):

given()
    .baseUri("https://api.example.com")
.when()
    .get("/users/123")
.then()
    .statusCode(200)
    .statusCode(in(Arrays.asList(200, 201)))  // Multiple valid codes
    .statusCode(lessThan(300));  // Any success code

JavaScript (with Chai):

const response = await fetch('https://api.example.com/users/123');
expect(response.status).to.equal(200);
expect(response.ok).to.be.true;  // 200-299 range

Python (requests):

response = requests.get('https://api.example.com/users/123')
assert response.status_code == 200
assert response.ok  # True for 200-399
response.raise_for_status()  # Raises exception if error

2. Response Body Validation

Exact Value Matching:

// REST Assured
.then()
    .body("status", equalTo("success"))
    .body("data.user.id", equalTo(123))
    .body("data.user.email", equalTo("john@example.com"));
// JavaScript
const data = await response.json();
expect(data.status).to.equal("success");
expect(data.data.user.id).to.equal(123);
# Python
data = response.json()
assert data["status"] == "success"
assert data["data"]["user"]["id"] == 123

Partial Matching:

.then()
    .body("email", containsString("@example.com"))
    .body("username", startsWith("user_"))
    .body("description", not(emptyOrNullString()));

Null and Existence Checks:

.then()
    .body("data.user", notNullValue())
    .body("data.user.id", not(emptyOrNullString()))
    .body("optionalField", nullValue())
    .body("$", hasKey("status"))
    .body("data.user", hasKey("email"));

3. Array Validation

Array Size:

.then()
    .body("users.size()", equalTo(10))
    .body("users.size()", greaterThan(0))
    .body("users.size()", lessThanOrEqualTo(100));

Array Contains:

.then()
    .body("users.id", hasItem(123))
    .body("users.id", hasItems(123, 456, 789))
    .body("roles", contains("admin", "user"))  // Exact order
    .body("roles", containsInAnyOrder("user", "admin"));  // Any order

All Elements Match:

.then()
    .body("users.email", everyItem(containsString("@")))
    .body("users.age", everyItem(greaterThan(0)))
    .body("users.status", everyItem(isIn(Arrays.asList("active", "inactive"))));

Array Filtering:

// Find elements matching condition
.then()
    .body("users.findAll { it.status == 'active' }.size()", greaterThan(0))
    .body("users.find { it.id == 123 }.email", equalTo("john@example.com"));

4. Schema Validation

JSON Schema (REST Assured):

import static io.restassured.module.jsv.JsonSchemaValidator.*;
 
.then()
    .body(matchesJsonSchemaInClasspath("user-schema.json"));

user-schema.json:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": ["status", "data"],
  "properties": {
    "status": {
      "type": "string",
      "enum": ["success", "error"]
    },
    "data": {
      "type": "object",
      "required": ["user"],
      "properties": {
        "user": {
          "type": "object",
          "required": ["id", "email", "username"],
          "properties": {
            "id": { "type": "integer" },
            "email": { 
              "type": "string",
              "format": "email"
            },
            "username": { 
              "type": "string",
              "minLength": 3,
              "maxLength": 20
            },
            "age": {
              "type": "integer",
              "minimum": 0,
              "maximum": 150
            }
          }
        }
      }
    }
  }
}

JavaScript (AJV):

const Ajv = require('ajv');
const ajv = new Ajv();
 
const schema = {
  type: 'object',
  required: ['status', 'data'],
  properties: {
    status: { type: 'string' },
    data: { type: 'object' }
  }
};
 
const validate = ajv.compile(schema);
const valid = validate(responseData);
expect(valid).to.be.true;

5. Header Validation

.then()
    .header("Content-Type", equalTo("application/json"))
    .header("Content-Type", containsString("application/json"))
    .header("X-Rate-Limit-Remaining", Integer::parseInt, greaterThan(0))
    .headers("Content-Type", "application/json",
             "Connection", "keep-alive");
expect(response.headers.get('content-type')).to.include('application/json');
expect(response.headers.get('x-rate-limit-limit')).to.exist;

6. Response Time Validation

.then()
    .time(lessThan(1000L))  // Less than 1 second
    .time(lessThan(1000L), TimeUnit.MILLISECONDS);
const start = Date.now();
const response = await fetch(url);
const duration = Date.now() - start;
expect(duration).to.be.below(1000);

7. Complex Nested Validation

Sample Response:

{
  "data": {
    "orders": [
      {
        "id": "ORD-001",
        "customer": {
          "name": "John Doe",
          "email": "john@example.com"
        },
        "items": [
          { "product": "Item A", "quantity": 2, "price": 10.00 },
          { "product": "Item B", "quantity": 1, "price": 20.00 }
        ],
        "total": 40.00,
        "status": "completed"
      }
    ]
  }
}

Validation:

.then()
    // Nested object validation
    .body("data.orders[0].customer.name", equalTo("John Doe"))
    .body("data.orders[0].customer.email", containsString("@"))
    
    // Nested array validation
    .body("data.orders[0].items.size()", equalTo(2))
    .body("data.orders[0].items.product", hasItems("Item A", "Item B"))
    .body("data.orders[0].items.quantity", everyItem(greaterThan(0)))
    
    // Calculated field validation
    .body("data.orders[0].total", equalTo(40.00f))
    
    // Complex filtering
    .body("data.orders[0].items.findAll { it.price > 15 }.size()", 
          equalTo(1));

8. Date/Time Validation

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
 
.then()
    .body("createdAt", notNullValue())
    .body("createdAt", matchesPattern("\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}.*"));
 
// Extract and parse
String dateStr = response.path("createdAt");
LocalDateTime date = LocalDateTime.parse(dateStr, 
    DateTimeFormatter.ISO_DATE_TIME);
assertTrue(date.isBefore(LocalDateTime.now()));

9. Custom Matchers

// REST Assured custom matcher
public static Matcher<String> isValidEmail() {
    return new BaseMatcher<String>() {
        @Override
        public boolean matches(Object item) {
            String email = (String) item;
            return email.matches("^[A-Za-z0-9+_.-]+@(.+)$");
        }
        
        @Override
        public void describeTo(Description description) {
            description.appendText("a valid email address");
        }
    };
}
 
// Usage
.then()
    .body("email", isValidEmail());

10. Validation Helper Functions

Reusable Validation Functions:

public class APIValidators {
    
    public static void validateSuccessResponse(Response response) {
        response.then()
            .statusCode(200)
            .body("status", equalTo("success"))
            .header("Content-Type", containsString("application/json"));
    }
    
    public static void validatePaginatedResponse(Response response, 
                                                   int expectedSize) {
        response.then()
            .statusCode(200)
            .body("data.size()", equalTo(expectedSize))
            .body("pagination.page", greaterThan(0))
            .body("pagination.total", greaterThanOrEqualTo(expectedSize));
    }
    
    public static void validateErrorResponse(Response response, 
                                              int statusCode, 
                                              String errorMessage) {
        response.then()
            .statusCode(statusCode)
            .body("status", equalTo("error"))
            .body("error.message", containsString(errorMessage));
    }
}
 
// Usage
Response response = given().get("/users");
APIValidators.validateSuccessResponse(response);

Quick Reference

Common Validation Patterns

PatternUse CaseExample
equalTo()Exact match.body("id", equalTo(123))
containsString()Partial match.body("msg", containsString("success"))
hasItem()Array contains.body("ids", hasItem(123))
everyItem()All elements.body("ages", everyItem(greaterThan(0)))
notNullValue()Not null.body("data", notNullValue())
hasKey()Has property.body("$", hasKey("status"))
matchesPattern()Regex.body("code", matchesPattern("[A-Z]{3}"))

Best Practices

Do:

  • Validate status code first
  • Check response structure (schema)
  • Validate critical fields
  • Test error responses
  • Use meaningful assertions
  • Extract and reuse common validations

Don't:

  • Only check status code
  • Ignore response headers
  • Skip error scenarios
  • Use vague assertions
  • Validate every single field (test what matters)

Key Takeaways

  • Always validate status code first
  • Use schema validation for structure verification
  • Test both positive and negative scenarios
  • Validate arrays with size and content checks
  • Check response times for performance
  • Create reusable validation functions
  • Validate headers and content types
  • Use custom matchers for complex validations

Comments (0)

Loading comments...