JSON, XML and JSON Schema Validation for API Testers
Almost every API response you test will be JSON, and banking or SOAP systems will hand you XML. To write good assertions you must read nested data fluently, know the exact data types, and be able to prove the whole structure is correct, not just one field. In this lesson you will learn JSON and XML from a tester’s point of view, how to extract values with JSONPath and XPath, and how JSON Schema validation catches the bugs that field-by-field checks miss.
JSON Basics in 5 Minutes
Here is a real response from the practice API: GET /orders/5001.
{
"data": {
"id": 5001,
"user_id": 1,
"status": "delivered",
"ordered_at": "2025-09-02T11:00:00Z",
"items": [
{ "product_id": 101, "name": "Wireless Mouse", "quantity": 2, "unit_price": 799, "line_total": 1598 },
{ "product_id": 108, "name": "Python Testing eBook", "quantity": 1, "unit_price": 349, "line_total": 349 }
],
"summary": { "item_count": 2, "subtotal": 1947, "tax": 350.46, "total": 2297.46, "currency": "INR" },
"shipping": { "city": "Pune", "pincode": "411001", "country": "IN" }
}
}
| JSON type | Example from this response | Tester’s note |
|---|---|---|
| String | "delivered", "411001" | Always double quotes. Pincode is a string (keeps leading zeros) |
| Number | 799, 350.46 | No quotes; integer and decimal are both “number” |
| Boolean | true / false | Lower case, no quotes. "true" is a string, a common bug |
| Null | null | “No value”. Different from missing key and from "" |
| Object | "summary": { ... } | Unordered key-value pairs |
| Array | "items": [ ... ] | Ordered list; may be empty [] |
subtotal started coming back as the string "1947". The website still displayed it fine, but the Android app added the coupon discount with string concatenation and showed a subtotal of “1947100”. Only a type check (or schema validation) catches this kind of bug.Reading Nested JSON: Paths
To assert a value you need its path. In JavaScript/TypeScript (Playwright) and in JSONPath:
| Value | TypeScript | JSONPath |
|---|---|---|
| Order status | body.data.status | $.data.status |
| First item name | body.data.items[0].name | $.data.items[0].name |
| All item names | body.data.items.map(i => i.name) | $.data.items[*].name |
| Grand total | body.data.summary.total | $.data.summary.total |
| Items costing over 500 | items.filter(i => i.unit_price > 500) | $.data.items[?(@.unit_price > 500)] |
Postman also uses this JavaScript syntax inside its test scripts: pm.response.json().data.status.
Assert Business Logic, Not Only Values
Good testers recompute numbers from the response itself:
- Each
line_total=quantity x unit_price(2 x 799 = 1598 ✔). subtotal= sum of line totals (1598 + 349 = 1947 ✔).tax= 18% of subtotal (1947 x 0.18 = 350.46 ✔).total= subtotal + tax (2297.46 ✔).item_count= length ofitems(2 ✔).
https://sqatools.in/wp-json/sqa-api/v1/orders/5001curl -i \
"https://sqatools.in/wp-json/sqa-api/v1/orders/5001"0.1 + 0.2 === 0.30000000000000004. Compare money with a tolerance (toBeCloseTo(2297.46, 2)) or in paise as integers, otherwise your test fails for the wrong reason.XML Basics for API Testers
Ask the practice API for XML using the Accept header:
https://sqatools.in/wp-json/sqa-api/v1/users/1curl -i \
-H "Accept: application/xml" \
"https://sqatools.in/wp-json/sqa-api/v1/users/1"<?xml version="1.0" encoding="UTF-8"?>
<response>
<data>
<id>1</id>
<name>Asha Verma</name>
<role>admin</role>
<active>true</active>
</data>
</response>
XML key concepts: elements (<name>), attributes (<price currency="INR">), namespaces (xmlns:soap="...", essential in SOAP), and every value is text until a schema (XSD) gives it a type.
XPath: extracting values from XML
| Goal | XPath |
|---|---|
| User name | /response/data/name |
| Name anywhere in the document | //name |
| Text value | //name/text() |
| Element with an attribute | //price[@currency='INR'] |
| SOAP body ignoring namespaces | //*[local-name()='AddResult'] |
SoapUI’s XPath Match assertion uses exactly these expressions; you will use them in the SoapUI lessons.
JSON vs XML
| JSON | XML | |
|---|---|---|
| Readability | Compact, easy to read | Verbose (opening and closing tags) |
| Data types | Built in (number, boolean, null…) | Everything is text; types come from XSD |
| Arrays | Native [ ] | Repeated elements |
| Attributes / namespaces | No | Yes |
| Comments | Not allowed | Allowed |
| Schema | JSON Schema | XSD |
| Query language | JSONPath | XPath, XQuery |
| Used in | REST, GraphQL, most modern APIs | SOAP, banking, insurance, legacy systems, RSS |
What Is JSON Schema?
A JSON Schema is a contract for JSON data. Instead of writing 30 assertions for 30 fields, you validate the whole response against one schema. The practice API publishes its schema for users:
https://sqatools.in/wp-json/sqa-api/v1/schema/usercurl -i \
"https://sqatools.in/wp-json/sqa-api/v1/schema/user"{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "user",
"type": "object",
"required": ["id", "name", "username", "email", "role", "city", "age", "active", "created_at"],
"properties": {
"id": { "type": "integer", "minimum": 1 },
"name": { "type": "string" },
"email": { "type": "string", "format": "email" },
"role": { "enum": ["admin", "editor", "viewer"] },
"age": { "type": "integer", "minimum": 18 },
"active": { "type": "boolean" },
"created_at": { "type": "string", "format": "date-time" }
},
"additionalProperties": false
}
Important JSON Schema keywords
| Keyword | Meaning | Bug it catches |
|---|---|---|
type | string, number, integer, boolean, object, array, null | “1947” instead of 1947 |
required | Keys that must exist | Field silently removed |
enum | Allowed values | New status “dispatched” the app doesn’t know |
format | email, date-time, uri, uuid… | Date returned as “12/11/2026” |
minimum / maximum | Number range | Negative price, age 0 |
minLength / maxLength / pattern | String rules | Phone with letters |
items, minItems | Array content and size | Order with zero items |
additionalProperties: false | No unknown fields | Leaked password_hash or internal fields |
nullable / "type": ["string","null"] | Allow null | Unexpected null crashing the app |
JSON Schema Validation in Playwright (with Ajv)
Install the most popular validator: npm i -D ajv ajv-formats.
import { test, expect } from '@playwright/test';
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
const BASE = 'https://sqatools.in/wp-json/sqa-api/v1';
const ajv = new Ajv({ allErrors: true });
addFormats(ajv);
test('every user matches the published JSON Schema', async ({ request }) => {
// 1. Get the contract from the API itself
const schema = await (await request.get(`${BASE}/schema/user`)).json();
const validate = ajv.compile(schema);
// 2. Validate all 12 users
const users = (await (await request.get(`${BASE}/users`, { params: { per_page: 50 } })).json()).data;
for (const user of users) {
const ok = validate(user);
expect(ok, JSON.stringify(validate.errors, null, 2)).toBe(true);
}
});
test('order totals are internally consistent', async ({ request }) => {
const { data: order } = await (await request.get(`${BASE}/orders/5001`)).json();
for (const item of order.items) {
expect(item.line_total).toBe(item.quantity * item.unit_price);
}
const subtotal = order.items.reduce((sum: number, i: any) => sum + i.line_total, 0);
expect(order.summary.subtotal).toBe(subtotal);
expect(order.summary.tax).toBeCloseTo(subtotal * 0.18, 2);
expect(order.summary.total).toBeCloseTo(subtotal + order.summary.tax, 2);
expect(typeof order.shipping.pincode).toBe('string');
});
When validation fails, Ajv tells you exactly what broke, for example /age must be integer or must NOT have additional properties: password_hash. TypeScript teams also use Zod schemas, which double as TypeScript types; you will use both in Module 6.
JSON Testing Checklist
- Response body is valid JSON (parsing doesn’t throw), even for errors.
- Correct data types: numbers aren’t strings, booleans aren’t “true”.
- Required fields present; no unexpected or sensitive fields.
- Null vs missing vs empty string behave as documented.
- Dates in one consistent format (ISO 8601 with time zone, e.g.
2025-09-02T11:00:00Z). - Arrays: empty list returns
[], notnull. - Calculated fields are correct; money compared safely.
- Unicode (Hindi names, emojis) survives a round trip.
- Malformed JSON in a request returns
400, not500.
Try the last check now. This request sends broken JSON (the body stops halfway), and a well-built API must answer 400 with a clear message:
https://sqatools.in/wp-json/sqa-api/v1/validate/usercurl -i \
-X POST \
-H "Content-Type: application/json" \
-d '{"name": "Asha", "email": ' \
"https://sqatools.in/wp-json/sqa-api/v1/validate/user"Common Mistakes
- Comparing a whole response with a saved copy: timestamps and ids change, so tests flake. Validate structure with a schema and assert only the stable values.
- Using
toEqualon objects when extra fields are allowed: usetoMatchObjectfor partial matching. - Assuming key order: JSON objects are unordered.
- Assuming array order when the API doesn’t promise one.
- Ignoring
additionalProperties: that’s how data leaks go unnoticed.
Interview Questions: JSON, XML and Schema
What are the data types supported by JSON?
String, number, boolean, null, object and array. There is no separate integer or date type in JSON itself; integers are numbers and dates are strings, which is why JSON Schema adds integer and format rules.
What is JSON Schema validation and why is it used?
It validates a JSON response against a schema that defines required fields, types, formats, ranges, enums and whether extra properties are allowed. It catches structural bugs such as missing fields, wrong types and leaked data in one assertion and is the basis of contract testing.
What is the difference between JSON and XML?
JSON is lighter, has native data types and arrays and is used by most modern REST APIs. XML is more verbose, supports attributes, namespaces and comments, uses XSD for types and XPath for queries, and is used by SOAP and many enterprise systems.
What is JSONPath?
A query language for JSON, similar to XPath for XML. For example, $.data.items[*].name returns all item names and $.data.items[?(@.unit_price > 500)] filters items by price.
What is the difference between null, an empty string and a missing key?
A missing key means the field isn’t sent at all, null means the field exists with no value, and an empty string is a value of length zero. Clients often handle them differently, so APIs must be consistent and tests should check the documented behaviour.
What does additionalProperties: false do?
It makes the schema reject any property not listed in properties. It helps detect unexpected fields, especially sensitive data such as password hashes or internal ids accidentally included in responses.
How do you validate an XML response?
Validate it against the XSD, use XPath assertions for specific values, check namespaces, and for SOAP verify there is no Fault element in positive scenarios. SoapUI provides Schema Compliance and XPath Match assertions for this.
How would you compare decimal values like prices in tests?
Use a tolerance-based comparison such as toBeCloseTo with 2 decimal places, or convert money to the smallest unit (paise) as integers, to avoid floating point precision errors.
Which library do you use for JSON Schema validation in TypeScript?
Ajv with ajv-formats is the most common choice. Zod is also popular because the same schema provides TypeScript types and runtime validation.
Why should you avoid comparing full responses with snapshots?
Dynamic values such as ids, timestamps and tokens change on every run, causing flaky failures. It’s better to validate structure with a schema and assert specific stable business values.