Resource library

QA How-To

Postman JSON Schema Validation Tutorial

Learn Postman JSON Schema Validation with a local API, nested response schemas, failure cases, error contracts, and a repeatable collection run with examples.

19 min read | 3,058 words

TL;DR

Define a JSON Schema object, then call pm.response.to.have.jsonSchema(schema) inside a named pm.test in Scripts > Post-response. Check status and media type separately, and use different schemas for success and expected error responses.

Key Takeaways

  • Assert schema with pm.response.to.have.jsonSchema(schema) in Scripts > Post-response.
  • Use required alongside properties; describing a field does not make it mandatory.
  • Validate nested line items with items, minItems, and object-level required rules.
  • Decide whether unknown properties should fail at each object level.
  • Give expected error responses their own status and schema tests.
  • Keep deliberate contract-defect requests outside the passing collection run.

Postman JSON Schema Validation checks whether a JSON response has the fields, types, and nested structure your API contract promises. You define a JSON Schema object in a post-response script and assert it with pm.response.to.have.jsonSchema(schema). This tutorial builds a local order API, validates a successful response, then deliberately changes individual fields so you can see exactly which rules catch a regression.

You will keep HTTP status checks separate from schema checks. A valid object returned with the wrong status is still a broken endpoint, and a 200 response containing the wrong data is still a failed contract. The examples use a deterministic local server, so you can repeat every test without depending on a public sample API. If Postman request creation is new to you, use the Postman tutorial for beginners as a companion.

What You Will Build

  • A local Node.js order endpoint with one valid order, several intentional contract defects, and a documented 404 error.
  • A saved Postman collection whose request scripts assert status, media type, JSON Schema, and one business rule.
  • A reusable schema stored at collection scope, including nested line items, optional notes, required keys, enums, and unknown-field policy.
  • A small diagnostic set that demonstrates missing-property, wrong-type, empty-array, and extra-property failures.
  • A one-iteration local Collection Runner check with only valid requests selected.

The success schema is deliberately narrow. It describes the consumer-facing response in this lab, not every field an order service might eventually have. You will see when strict additionalProperties rules are helpful and when they make a test too sensitive. For wider contract concepts, read the contract testing guide after completing the local exercise.

Prerequisites

Use Postman desktop v12 and Node.js v22.0.0 or later, then record the exact versions actually installed on your machine. Those are published release versions, not fabricated patch pins; use the current patch for your installed branch. In Postman, open Help > About Postman for its exact version. In a terminal, run:

node --version
curl --version

The Node command should report v22.0.0 or later. curl is used for independent response checks; no npm dependency or Docker image is required. Create an empty directory named postman-schema-lab and put the server.mjs file from Step 1 inside it. Open Postman desktop on the same machine as the server. A cloud monitor cannot call your computer's 127.0.0.1; the web app requires an agent that can reach localhost.

Piece Role Check before continuing
Node.js Runs the built-in HTTP fixture node --version succeeds
curl Shows raw status and body outside Postman curl --version succeeds
Postman desktop Sends requests and runs scripts Help > About shows your installed version
Local Collection Runner Replays saved requests Functional and Local options are available

Postman's test script documentation places the code in Scripts > Post-response. That script runs after the response arrives. The examples below use the built-in schema assertion described in the Postman response reference; you do not need to install a validator in the collection.

Step 1: Start a Controlled Order API

Create server.mjs with this complete Node.js file. The normal route returns the same order on every GET. A case query parameter creates one defect at a time, while /orders/999 returns a separate error representation. The server binds only to loopback and holds no persistent data.

import { createServer } from 'node:http';

const baseOrder = {
  orderId: 42,
  status: 'paid',
  currency: 'USD',
  placedOn: '2026-10-05',
  note: 'Leave at reception',
  lines: [
    { sku: 'BK-100', quantity: 2, unitPrice: 12.5 }
  ],
  total: 25
};

function sendJson(res, status, value) {
  res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' });
  res.end(JSON.stringify(value));
}

const server = createServer((req, res) => {
  const url = new URL(req.url, 'http://127.0.0.1:4011');

  if (req.method === 'GET' && url.pathname === '/orders/999') {
    return sendJson(res, 404, {
      error: { code: 'ORDER_NOT_FOUND', message: 'No order 999' }
    });
  }

  if (req.method === 'GET' && url.pathname === '/orders/42') {
    const order = structuredClone(baseOrder);
    switch (url.searchParams.get('case')) {
      case 'missing-id': delete order.orderId; break;
      case 'string-price': order.lines[0].unitPrice = '12.50'; break;
      case 'empty-lines': order.lines = []; break;
      case 'extra': order.debug = true; break;
      case 'no-note': delete order.note; break;
      case 'null-note': order.note = null; break;
    }
    return sendJson(res, 200, order);
  }

  sendJson(res, 404, {
    error: { code: 'ROUTE_NOT_FOUND', message: 'Unknown route' }
  });
});

server.listen(4011, '127.0.0.1', () => {
  console.log('Order API ready at http://127.0.0.1:4011');
});

Run node server.mjs and leave that terminal open. The fixture copies baseOrder for every request, so testing ?case=missing-id never damages the next response. It intentionally sends string-price with HTTP 200: status alone cannot detect a wrong field type. The unknown-route branch has a different error code from the missing-order branch, making a typo visible in a later negative test.

Verify Step 1: In a second terminal, run:

node --check server.mjs
curl -i http://127.0.0.1:4011/orders/42
curl -i http://127.0.0.1:4011/orders/999

The first request should return 200 with orderId 42 and one line. The second should return 404 with ORDER_NOT_FOUND. If node --check fails, fix the file before opening Postman; a Postman network error cannot diagnose JavaScript syntax in the server.

Step 2: Begin Postman JSON Schema Validation With a Small Contract

Create a collection named Order schema lab and save a GET request named Valid order at http://127.0.0.1:4011/orders/42. Open Scripts > Post-response on that request. Start with a two-field schema instead of copying a giant response into an assertion. Paste this script:

const schema = {
  type: 'object',
  properties: {
    orderId: { type: 'integer' },
    status: { type: 'string' }
  },
  required: ['orderId', 'status']
};

pm.test('Valid order returns 200', function () {
  pm.expect(pm.response.code).to.equal(200);
});

pm.test('Valid order uses JSON', function () {
  pm.expect(pm.response.headers.get('Content-Type'))
    .to.include('application/json');
});

pm.test('Valid order meets the starter schema', function () {
  pm.response.to.have.jsonSchema(schema);
});

properties describes allowed checks for named keys; it does not make them mandatory. required is what rejects an absent orderId or status. The integer type rejects a quoted numeric ID even if a person can read it as 42. The media-type assertion uses include because the fixture also sends charset=utf-8. The status, header, and schema are separate named tests, so their failures point to different layers.

The schema assertion accepts a JavaScript object, not a string containing JSON. Postman's documented jsonSchema assertion uses Ajv for validation. Keep this starter schema visible while learning what each keyword does. Its gaps are intentional: it does not yet inspect line items, constrain status values, or reject extra fields. If your first response changes only total, these three tests still pass, which is a useful reminder that a test can only enforce rules you actually wrote.

Verify Step 2: Click Send and read Test Results; expect three passes. Compare the raw response with curl -i http://127.0.0.1:4011/orders/42. To prove the schema test is active, temporarily point the Postman URL to http://127.0.0.1:4011/orders/42?case=missing-id, Send, and look for the schema failure. Restore the saved URL before the next step.

Step 3: Define Nested Fields, Required Keys, and Allowed Values

Move the full success contract into the collection's Scripts > Pre-request tab. A collection pre-request script runs before each request, so each request can read the same schema. Paste the complete definition below. It has a Draft-07 identifier and uses keywords supported by the validator behind Postman's built-in assertion. No remote $ref or external package is needed.

const orderSchema = {
  $schema: 'http://json-schema.org/draft-07/schema#',
  type: 'object',
  additionalProperties: false,
  properties: {
    orderId: { type: 'integer', minimum: 1 },
    status: { type: 'string', enum: ['pending', 'paid'] },
    currency: { type: 'string', enum: ['USD'] },
    placedOn: { type: 'string', pattern: '^\\d{4}-\\d{2}-\\d{2}
#39; }, note: { type: ['string', 'null'], minLength: 1 }, lines: { type: 'array', minItems: 1, items: { type: 'object', additionalProperties: false, properties: { sku: { type: 'string', minLength: 1 }, quantity: { type: 'integer', minimum: 1 }, unitPrice: { type: 'number', minimum: 0 } }, required: ['sku', 'quantity', 'unitPrice'] } }, total: { type: 'number', minimum: 0 } }, required: ['orderId', 'status', 'currency', 'placedOn', 'lines', 'total'] }; pm.collectionVariables.set('orderSchema', JSON.stringify(orderSchema));

Replace the Valid order post-response script from Step 2 with this version. The schema is parsed from the collection variable, so the success request has one source of truth. Explicit pm.collectionVariables.get avoids a same-named environment variable silently overriding it. The extra total assertion checks a relationship JSON Schema does not express here.

pm.test('Valid order returns 200 with JSON', function () {
  pm.expect(pm.response.code).to.equal(200);
  pm.expect(pm.response.headers.get('Content-Type'))
    .to.include('application/json');
});

pm.test('Valid order matches the shared schema', function () {
  const schema = JSON.parse(pm.collectionVariables.get('orderSchema'));
  pm.response.to.have.jsonSchema(schema);
});

pm.test('Order total matches its line items', function () {
  const order = pm.response.json();
  const calculated = order.lines.reduce(
    (sum, line) => sum + line.quantity * line.unitPrice, 0
  );
  pm.expect(order.total).to.equal(calculated);
});

Notice the boundaries. The enum allows only the two documented statuses, and minItems: 1 rejects an empty order. Both top-level and line-item additionalProperties: false reject fields absent from this contract. The simple date pattern checks the shape YYYY-MM-DD, not calendar validity: 2026-99-99 would match it. Use an explicit date or business-rule test if the API promises real calendar dates. For a broader discussion of schema rules across tools, see validating JSON response schemas.

Verify Step 3: Send Valid order and expect three passes. Run curl -s http://127.0.0.1:4011/orders/42 to confirm the one-line total is 25. In Postman, inspect the collection variable orderSchema; it should contain a JSON string beginning with $schema. If it is missing, confirm the code is on the collection's Pre-request tab and that the request was saved inside that collection.

Step 4: Check Optional, Null, and Strictness Semantics

The note property is defined in properties but omitted from the top-level required list. That makes absence valid. When present, it may be a nonempty string or null. These are distinct cases for clients: a missing key can mean no note was supplied, while an explicit null can mean the server knows there is no note. Your API contract must decide whether both are acceptable.

Duplicate Valid order twice. Name the copies No note and Null note, and set their URLs to http://127.0.0.1:4011/orders/42?case=no-note and http://127.0.0.1:4011/orders/42?case=null-note. Keep their copied post-response script from Step 3. For No note, append this request-specific test:

pm.test('No note omits the optional key', function () {
  pm.expect(pm.response.json()).to.not.have.property('note');
});

For Null note, append a different test:

pm.test('Null note is explicit', function () {
  pm.expect(pm.response.json().note).to.equal(null);
});

Both responses match the same schema, and each request asserts its intended meaning. The type array on note is not equivalent to leaving its type unconstrained. A number or empty string would fail. Likewise, additionalProperties: false is local to the object where you place it; setting it only at the top does not stop an unexpected property inside each lines item. This is why the full schema declares the rule twice.

Consider strictness before copying this pattern into a live service. Rejecting unrecognized fields is suitable when an exact wire representation is part of the contract or when unknown fields would indicate accidental data disclosure. It can be disruptive when the API explicitly permits additive fields. In that case, remove additionalProperties: false at the relevant object level, and keep precise assertions for keys consumers rely on. The comparison is practical:

Contract choice What catches What may cause noise
Required keys only Missing promised data Extra fields pass silently
Types and value limits Wrong shapes and illegal values Relationships still need separate tests
additionalProperties: false Unexpected keys at that object level Harmless additive fields fail
Separate arithmetic assertion Inconsistent order total Floating-point policies need care in real money APIs

Verify Step 4: Send each new request and expect four passes per request. Inspect the two raw bodies with curl -s 'http://127.0.0.1:4011/orders/42?case=no-note' and curl -s 'http://127.0.0.1:4011/orders/42?case=null-note'. One body has no note key; the other contains "note":null. Do not infer the difference from a visual blank in Postman's response viewer.

Step 5: Probe Postman JSON Schema Validation Failures

Now create four diagnostic copies of Valid order in a folder named Contract defects. Keep the Step 3 post-response script on each copy, and change only the URL's case value. These requests are designed to show red tests. Exclude this folder from the final green Collection Runner run; otherwise an intentionally malformed fixture would be reported as a regression.

Diagnostic request URL suffix Rule that fails
Missing ID ?case=missing-id Top-level required
String price ?case=string-price Nested unitPrice number type
Empty lines ?case=empty-lines minItems: 1
Extra debug field ?case=extra Top-level additionalProperties: false

For example, string-price returns "unitPrice":"12.50". The number looks reasonable to a human, but it is a string in JSON. The schema catches that even though the status is 200 and the Content-Type is correct. empty-lines is worth testing separately because an element-level check alone can pass vacuously when an array contains no elements. missing-id proves why properties without required would have been insufficient. extra shows a policy failure, not a type failure.

When a test fails, select its row in Test Results and inspect the assertion message. Postman's Console is useful for checking the actual value if the message is terse. Avoid changing the schema merely to turn a red result green; first decide whether the response or the intended contract is wrong. For a real API, compare the documented OpenAPI response shape as well. The OpenAPI schema testing guide covers that neighboring workflow.

Verify Step 5: Send each diagnostic request once and expect the schema test to fail. Independently inspect the raw fixtures with these commands:

curl -s 'http://127.0.0.1:4011/orders/42?case=missing-id'
curl -s 'http://127.0.0.1:4011/orders/42?case=string-price'
curl -s 'http://127.0.0.1:4011/orders/42?case=empty-lines'
curl -s 'http://127.0.0.1:4011/orders/42?case=extra'

The status and JSON tests should remain green on these four requests, because each defect is in the payload contract. The business-rule total test also fails for empty-lines. It can pass for string-price because JavaScript multiplies numeric strings; the schema is the check that catches the wrong JSON type. Do not run the diagnostic folder as part of a passing release gate.

Step 6: Give Error Responses Their Own Schema

Save a GET request named Missing order at http://127.0.0.1:4011/orders/999. Its HTTP 404 is expected, and its body has an error object rather than success fields. Applying the success schema to it would fail for the wrong reason. Put this complete post-response script on the request:

const errorSchema = {
  $schema: 'http://json-schema.org/draft-07/schema#',
  type: 'object',
  additionalProperties: false,
  properties: {
    error: {
      type: 'object',
      additionalProperties: false,
      properties: {
        code: { type: 'string', enum: ['ORDER_NOT_FOUND'] },
        message: { type: 'string', minLength: 1 }
      },
      required: ['code', 'message']
    }
  },
  required: ['error']
};

pm.test('Missing order returns 404', function () {
  pm.expect(pm.response.code).to.equal(404);
});

pm.test('Missing order has the documented error body', function () {
  pm.response.to.have.jsonSchema(errorSchema);
});

A passing 404 test says the API handled this negative scenario correctly. The enum on code distinguishes a missing record from a typo in the URL, where the fixture returns ROUTE_NOT_FOUND. The error message only needs to be a nonempty string; client logic should depend on the stable machine code, not exact English text. If a real endpoint uses different error shapes for 400, 401, 404, and 500, give each documented class an appropriate schema or a clearly modeled union. Do not make a single permissive schema that accepts every error without checking which one occurred.

Verify Step 6: Send Missing order and expect two passes. Run curl -i http://127.0.0.1:4011/orders/999 to compare status and body. Temporarily change the saved path to /order/999, Send, and observe that the schema test rejects ROUTE_NOT_FOUND even though the status remains 404. Restore /orders/999 afterward.

Step 7: Run the Passing Collection and Review the Contract

Arrange the saved passing requests as Valid order, No note, Null note, and Missing order. Keep the four defect requests in their separate folder. Open the collection's Run action, choose Functional and Local, select the four passing requests, set one iteration, and start. Postman's Collection Runner guide documents the current run controls and result view.

The expected count is thirteen passing named tests: three on Valid order, four each on No note and Null note, and two on Missing order. If your copied scripts have another named test, use the displayed per-request counts instead of treating thirteen as a hard-coded universal number. There should be zero failures. Save the collection after editing scripts so the runner does not use an older request version.

This local run proves both success and expected error contracts, but it does not prove the endpoint satisfies every business invariant. The schema permits total: 0 even with a positive-priced line; the separate arithmetic assertion catches that mismatch on success requests. It also permits any date-shaped string, so add a calendar check if the API promises actual dates. Monetary values in a production API may use integer minor units or decimal strings rather than IEEE floating-point numbers; adapt the schema and arithmetic check to the documented representation.

Verify Step 7: Run curl -i http://127.0.0.1:4011/orders/42 once more to ensure the fixture is still available, then inspect the Collection Runner summary for zero failed tests. If a local run cannot reach the server, check that node server.mjs is still active and that you selected Local. A cloud execution context cannot use your laptop's loopback address.

Troubleshooting

  • Problem: Could not get response or ECONNREFUSED -> Restart node server.mjs, then check curl -i http://127.0.0.1:4011/orders/42. Verify port 4011 and the path in Postman before debugging schema code.
  • Problem: JSON.parse receives undefined for orderSchema -> Save the request inside Order schema lab, place the full schema code on the collection's Pre-request tab, and resend. Reading with pm.collectionVariables.get specifically avoids scope-precedence surprises.
  • Problem: A field appears in properties but its absence passes -> Add its name to required on that same object. For a nested line item, put the key in the line-item object's required, not the top-level list.
  • Problem: An extra nested line property passes -> Put additionalProperties: false inside the items schema. A rule on the order object does not propagate into child objects.
  • Problem: pm.response.json() fails while checking a schema -> Inspect the actual status, media type, and raw body. A proxy HTML page or wrong URL is not a JSON contract failure; correct the request or server response first.
  • Problem: Diagnostic requests make the full run red -> Exclude the Contract defects folder from the passing run. Use those requests for deliberate failure demonstrations, then run the four documented scenarios as the green suite.

For more assertion patterns around status, headers, and body values, see Postman collection variables and scopes and the API testing roadmap. When reporting a failed schema test to an API owner, include the request URL, status, failing schema path, actual payload fragment, and the contract version. A screenshot of a red badge without the failing field is hard to act on.

Interview Questions and Answers

A good interview explanation separates syntax from contract design. Say which response shape you are validating, why a keyword is present, and how a specific change would fail. The interviewQnA entries below cover six common prompts about required, nested arrays, optional nulls, error schemas, strictness, and business rules. Practice answering with the fixture's case variants rather than reciting a generic definition of JSON Schema.

Common Mistakes and Better Checks

  • Copying one response wholesale as a fixed expected object makes tests fail on harmless dynamic values. Write types, required keys, enums, and bounds that express the actual promise.
  • Defining a property without putting it in required does not require the key. Use the missing-ID fixture to demonstrate the difference.
  • Reusing a success schema for 404 responses hides whether the error contract is correct. Give documented error shapes their own tests.
  • Enabling additionalProperties: false everywhere without discussing API evolution can turn additive changes into noisy failures. Choose strictness per object and consumer need.
  • Treating schema validation as a complete behavior test misses arithmetic, authorization, persistence, and status semantics. Pair the schema with focused assertions for those rules.
  • Weakening a failing schema to accept a string where a number is documented can conceal a server regression. Inspect the payload and source contract before revising an expectation.

Where To Go Next

You now have Postman JSON Schema Validation for a nested success response and an expected error response, plus controlled examples that show what each important schema keyword catches. Export the passing collection for review, record the Postman and Node versions used, and run the local collection again after any schema change. Keep the defect folder as a teaching aid, outside your release gate.

Extend the exercise with Postman data-driven testing when one schema should cover several input rows. For CI, follow Postman Newman in CI and point the runner at a service reachable from that environment instead of 127.0.0.1 on your workstation. If your team tests the same contract in Java, compare the REST Assured JSON Schema validation tutorial. Start with one real endpoint and write down which properties are required, optional, nullable, or intentionally extensible before adapting this schema.

Interview Questions and Answers

How would you validate an API response against JSON Schema in Postman?

I save a request and place a named pm.test in Scripts > Post-response. Inside it, I pass a schema object to pm.response.to.have.jsonSchema(schema). I assert status and Content-Type separately so a transport problem is easy to distinguish from a payload contract failure.

Why can a property be missing even though it appears in the schema properties object?

The properties keyword says how to validate a value if that key exists. Required keys are declared in a separate required array on the same object. I demonstrate the distinction with a response that omits one promised identifier.

How do you validate every object inside a response array?

I put an object schema under the array's items keyword, including field types and the line-item required list. I also set minItems when an empty array would be invalid. If extra keys are forbidden inside each item, I set additionalProperties there rather than only on the parent.

How would you model an optional field that may explicitly be null?

I leave the field out of required and define its type as a union including null. I test an omitted-key response and a present-null response separately if callers make different decisions for them. An empty string remains a separate case and can be rejected with minLength.

What is the trade-off of additionalProperties: false?

It catches unexpected keys and can surface accidental exposure of fields. It also rejects compatible additive fields when the API contract permits extension. I decide strictness at each object level from the published contract and consumer expectations.

Why should an error response use a different schema from a success response?

Error bodies often have a code and message rather than success fields. I assert the expected status and a dedicated error shape, including a stable machine-readable code. That distinguishes a missing record from a misspelled route that also returns 404.

What does a JSON Schema test miss in an order response?

A basic schema checks structure, types, and local constraints, but it does not prove the total equals quantity times price or that the order was persisted. I add targeted behavioral assertions for those promises. I also avoid claiming a simple date regex proves calendar validity.

Frequently Asked Questions

How do I validate a JSON response schema in Postman?

Put a JavaScript schema object in a request or collection script, then call pm.response.to.have.jsonSchema(schema) inside pm.test on the request's Scripts > Post-response tab. Send the request and inspect Test Results for the named assertion.

Does listing a field under properties make it required?

No. The properties keyword applies validation if the field appears; the required array rejects an absent key. Put required at the same object level as the property it names.

How can Postman reject an empty JSON array?

Set minItems to the smallest acceptable count in the array schema, such as 1 for an order that must contain a line. The items schema checks elements that exist but does not itself guarantee any element exists.

What is the difference between an omitted key and null in JSON Schema?

An omitted optional key is permitted when it is absent from required. An explicit null is permitted only if its property schema allows null, such as type: ['string', 'null']. Test both cases when clients treat them differently.

Should I set additionalProperties to false in every schema?

Use it when unexpected fields violate a documented response contract or could expose data. If additive fields are supported, leave the relevant object extensible and assert the fields consumers need. The rule must be set separately on nested objects.

Can a Postman test pass when the API returns 404?

Yes. For a missing-resource request, assert the expected 404 and validate its documented error schema. A passing test confirms that this negative behavior is correct.

Why do I need a business assertion after schema validation?

A schema can require numeric total and line prices without proving that the total equals the sum of line amounts. Add a focused calculation or other behavior assertion for that relationship.

Related Guides