JSON API Testing: Postman Collections, Newman, and Contract Tests

Last updated:

JSON API testing validates that endpoints return correct status codes, JSON structure, and values — automated tests catch regressions before they reach production.

Postman collection JSON defines requests, assertions (pm.test), and environment variables — export as collection.json and run headlessly with newman run collection.json. A JSON response assertion example: pm.expect(pm.response.json().data.id).to.be.a('number').

This guide covers Postman collection JSON format, Newman CLI for CI, supertest JSON assertions in Node.js, contract testing with Dredd, JSON response schema validation in tests, and Playwright API testing.

Postman Collection JSON Format

A Postman collection exports as a single JSON file. The v2.1 format is the current standard — confirm the schema URL in the info object before committing the file to version control.

{
  "info": {
    "name": "Users API",
    "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
  },
  "item": [
    {
      "name": "Get user by ID",
      "request": {
        "method": "GET",
        "url": {
          "raw": "{{baseUrl}}/users/{{userId}}",
          "host": ["{{baseUrl}}"],
          "path": ["users", "{{userId}}"]
        },
        "header": [
          { "key": "Accept", "value": "application/json" }
        ]
      },
      "event": [
        {
          "listen": "test",
          "script": {
            "exec": [
              "pm.test('status is 200', () => {",
              "  pm.response.to.have.status(200);",
              "});",
              "pm.test('body has id', () => {",
              "  const json = pm.response.json();",
              "  pm.expect(json.data.id).to.be.a('number');",
              "  pm.expect(json.data.email).to.be.a('string');",
              "});"
            ],
            "type": "text/javascript"
          }
        }
      ]
    }
  ]
}

The item array holds request objects. Each request has a request block (method, URL, headers, body) and an event array. The test event script runs after the response arrives; the prerequest event script runs before the request is sent — use it for auth token generation or data setup.

Environment variables like {{}baseUrl}}} are resolved at runtime from an environment file. Keep base URLs, API keys, and IDs out of the collection JSON itself — this lets the same collection run against dev, staging, and production by swapping the environment file. Export the environment from Postman as environment.json alongside the collection.

Group requests into folders (nested item arrays with a name field) by resource or feature. Folder-level event scripts run for every request in the folder, which is useful for adding auth headers without repeating them on each request item.

Newman CLI for CI/CD

Newman is the official headless Postman runner. It executes a collection JSON file and reports test results — no Postman GUI required.

# Install Newman
npm install -g newman
# or as a dev dependency
npm install --save-dev newman newman-reporter-htmlextra

# Basic run
newman run collection.json

# Run with environment, multiple reporters, and JUnit output
newman run collection.json \
  -e environment.json \
  --reporters cli,junit,htmlextra \
  --reporter-junit-export results/junit.xml \
  --reporter-htmlextra-export results/report.html

# Run a specific folder only
newman run collection.json -e environment.json --folder "Users API"

# Set iteration count for data-driven tests
newman run collection.json -e environment.json -d test-data.json --iteration-count 5
# .github/workflows/api-tests.yml
name: API Tests
on: [push, pull_request]
jobs:
  api-test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npx newman run postman/collection.json -e postman/env.staging.json --reporters cli,junit --reporter-junit-export results.xml
      - uses: actions/upload-artifact@v4
        if: always()
        with:
          name: newman-results
          path: results.xml

Newman exits with code 0 when all tests pass and code 1 when any test fails. CI systems (GitHub Actions, Jenkins, GitLab CI, CircleCI) detect non-zero exit codes and mark the build as failed — no extra configuration needed.

The -d test-data.jsonflag accepts a JSON array of objects; Newman iterates the collection once per array element, substituting each object's keys as variables. This enables data-driven API testing — test 10 different user IDs or request payloads from a single collection run.

supertest JSON Assertions

supertest wraps any Node.js HTTP app (Express, Fastify, Koa) in a test HTTP server — no port binding, no running server required. Tests start faster and avoid port conflicts in CI.

// Install
npm install --save-dev supertest @types/supertest

// users.test.ts
import request from 'supertest'
import app from '../src/app' // Express app, NOT app.listen()

describe('GET /users/:id', () => {
  it('returns 200 and JSON body for valid ID', async () => {
    const res = await request(app)
      .get('/users/1')
      .set('Accept', 'application/json')
      .expect(200)
      .expect('Content-Type', /json/)

    // Assert on parsed JSON body
    expect(res.body).toHaveProperty('id', 1)
    expect(res.body).toHaveProperty('email')
    expect(typeof res.body.email).toBe('string')
  })

  it('returns 404 with error JSON for missing user', async () => {
    const res = await request(app)
      .get('/users/99999')
      .expect(404)
      .expect('Content-Type', /json/)

    expect(res.body).toHaveProperty('error')
    expect(res.body.error).toMatch(/not found/i)
  })
})

describe('POST /users', () => {
  it('creates a user and returns 201 with Location header', async () => {
    const res = await request(app)
      .post('/users')
      .send({ name: 'Alice', email: 'alice@example.com', role: 'user' })
      .expect(201)
      .expect('Content-Type', /json/)

    expect(res.body).toHaveProperty('id')
    expect(res.headers['location']).toMatch(/\/users\/\d+/)
  })
})

Chain .expect(statusCode) and .expect('Header', value) before await — supertest accumulates assertions and throws on the first failure. Access res.body for the parsed JSON body (supertest auto-parses application/json responses). Use Jest's toHaveProperty, toMatchObject, or toBeInstanceOf for body assertions.

Export your Express app without calling app.listen() in the module. Call listen() only in a separate entry point (e.g. server.ts). supertest calls app.listen(0) internally, picks a random free port, and closes the server after each test — this prevents port conflicts when running tests in parallel.

Contract Testing with Dredd

Dredd reads your OpenAPI JSON (or YAML) spec and generates HTTP test requests from the example values defined in the schema. It then sends those requests to your running server and asserts the responses match the documented schemas.

# Install Dredd globally
npm install -g dredd

# Initialize config (interactive)
dredd init

# Run against a local server
dredd openapi.json http://localhost:3000

# dredd.yml (generated by dredd init)
dry-run: null
hookfiles: null
language: nodejs
sandbox: false
server: node src/server.js
server-wait: 3
reporter: cli
custom: {}
names: false
only: []
color: true
level: info
timestamp: false
silent: false
path: []
blueprint: openapi.json
endpoint: 'http://localhost:3000'
// hooks.js — modify transactions before Dredd sends them
const hooks = require('hooks')

// Add auth header to all requests
hooks.beforeAll((transactions, done) => {
  transactions.forEach(transaction => {
    transaction.request.headers['Authorization'] = 'Bearer test-token'
  })
  done()
})

// Skip a specific transaction
hooks.before('Users > Get user > 200', (transaction, done) => {
  transaction.skip = true
  done()
})

Dredd's transaction object gives you full control over the request before it is sent and the response before it is validated. Use hooks to inject auth tokens, seed test data, or skip transactions that require complex setup. Hook files are loaded with --hookfiles hooks.js.

In CI, run Dredd against a test database by setting environment variables before the dredd command. Dredd exits non-zero on any failed assertion, making it a reliable gate for catching schema drift between the OpenAPI spec and the actual implementation. Pair it with JSON OpenAPI documentation to keep the spec accurate.

JSON Response Schema Validation in Tests

Status code assertions only verify the HTTP layer. Schema validation in tests verifies the JSON body structure — catching missing fields, wrong types, and unexpected nulls that status checks miss.

// Using Ajv directly in tests
import Ajv from 'ajv'
import addFormats from 'ajv-formats'
import request from 'supertest'
import app from '../src/app'

const ajv = new Ajv()
addFormats(ajv)

const userResponseSchema = {
  type: 'object',
  required: ['id', 'email', 'name', 'createdAt'],
  properties: {
    id:        { type: 'integer' },
    email:     { type: 'string', format: 'email' },
    name:      { type: 'string', minLength: 1 },
    createdAt: { type: 'string', format: 'date-time' },
  },
  additionalProperties: false,
}

const validate = ajv.compile(userResponseSchema)

it('GET /users/:id response matches schema', async () => {
  const res = await request(app).get('/users/1').expect(200)
  const valid = validate(res.body)
  if (!valid) console.error(validate.errors)
  expect(valid).toBe(true)
})
// Using chai-json-schema (Mocha / Chai style)
const chai = require('chai')
const chaiJsonSchema = require('chai-json-schema')
chai.use(chaiJsonSchema)
const { expect } = chai

it('response body matches user schema', async () => {
  const res = await request(app).get('/users/1')
  expect(res.body).to.be.jsonSchema({
    type: 'object',
    required: ['id', 'email'],
    properties: {
      id:    { type: 'integer' },
      email: { type: 'string', format: 'email' },
    },
  })
})

Compile the Ajv validator once outside the test block to avoid recompilation overhead on every test run. Store response schemas in a schemas/responses/ directory alongside your request schemas so they can be shared between test files and OpenAPI documentation generation.

See the JSON Schema testing guide for detailed patterns on Ajv error assertions, positive/negative test cases, and snapshot testing error output.

Playwright API Testing with JSON

Playwright's API testing context runs HTTP requests without a browser. It shares the same test runner, fixtures, and reporter as browser tests — no separate framework needed.

// playwright.config.ts
import { defineConfig } from '@playwright/test'

export default defineConfig({
  use: {
    baseURL: 'https://api.example.com',
    extraHTTPHeaders: {
      'Accept': 'application/json',
    },
  },
})
// users.api.spec.ts
import { test, expect } from '@playwright/test'

test('GET /users/1 returns user JSON', async ({ request }) => {
  const response = await request.get('/users/1')
  expect(response).toBeOK() // asserts 2xx status
  expect(response.headers()['content-type']).toMatch(/json/)

  const body = await response.json()
  expect(body.id).toBe(1)
  expect(typeof body.email).toBe('string')
})

test('POST /users creates a user', async ({ request }) => {
  const response = await request.post('/users', {
    data: { name: 'Bob', email: 'bob@example.com', role: 'user' },
  })
  expect(response.status()).toBe(201)

  const body = await response.json()
  expect(body).toMatchObject({ name: 'Bob', email: 'bob@example.com' })
  expect(body.id).toBeDefined()
})

test('DELETE /users/:id returns 204 with no body', async ({ request }) => {
  const response = await request.delete('/users/1')
  expect(response.status()).toBe(204)
  // 204 No Content — body should be empty
  const text = await response.text()
  expect(text).toBe('')
})

expect(response).toBeOK() asserts the status code is in the 200–299 range. For exact status codes, use expect(response.status()).toBe(201). Call await response.json() to parse the body — Playwright reads the response stream once, so call json() or text() only once per response.

Playwright API tests are faster than browser tests because they skip the browser launch. Run API tests and browser E2E tests in the same CI step using separate test file globs in playwright.config.ts. Use the request fixture for stateless API calls, or create a named APIRequestContext fixture to share an authenticated session across tests.

Test Data Management with JSON Fixtures

Hardcoding test payloads inside test files makes them brittle and hard to reuse. JSON fixture files centralize test data and make parametrized testing straightforward.

// fixtures/users.json
{
  "valid": [
    { "name": "Alice", "email": "alice@example.com", "role": "user" },
    { "name": "Bob",   "email": "bob@example.com",   "role": "admin" }
  ],
  "invalid": [
    { "name": "",    "email": "alice@example.com", "role": "user" },
    { "name": "Eve", "email": "not-an-email",       "role": "user" },
    { "name": "Mal", "email": "mal@example.com",    "role": "superuser" }
  ]
}
// Parametrized tests with fixture data (Jest)
import fixtures from '../fixtures/users.json'
import request from 'supertest'
import app from '../src/app'

describe('POST /users — valid inputs', () => {
  fixtures.valid.forEach((payload, i) => {
    it(`accepts valid user ${i + 1}: ${payload.name}`, async () => {
      await request(app).post('/users').send(payload).expect(201)
    })
  })
})

describe('POST /users — invalid inputs', () => {
  fixtures.invalid.forEach((payload, i) => {
    it(`rejects invalid user ${i + 1}: ${payload.email}`, async () => {
      await request(app).post('/users').send(payload).expect(400)
    })
  })
})
// Factory pattern for flexible test object creation
function buildUser(overrides = {}) {
  return {
    name:  'Default User',
    email: 'user@example.com',
    role:  'user',
    ...overrides,
  }
}

// Use in tests — override only what matters for the specific assertion
it('rejects missing name', async () => {
  await request(app)
    .post('/users')
    .send(buildUser({ name: '' }))
    .expect(400)
})

it('rejects invalid role', async () => {
  await request(app)
    .post('/users')
    .send(buildUser({ role: 'superuser' }))
    .expect(400)
})

The factory pattern keeps tests focused — each test overrides only the field relevant to the assertion, making the intent immediately clear. Combine factories with fixture files: load baseline objects from JSON files and apply overrides inline in the test. See the JSON data validation guide for validation patterns that complement fixture-driven testing.

For large fixture datasets, store them in __fixtures__/ (Jest convention) or tests/fixtures/ and load them with require() or fs.readFileSync(). Keep fixture files small and focused — one fixture file per resource type. Avoid sharing mutable fixture objects across tests; always spread or clone before modifying.

Definitions

Postman collection
A JSON file that groups API requests, test scripts, pre-request scripts, and environment variable references into a portable, version-controllable test suite that can be run in the Postman GUI or headlessly with Newman.
Newman
The official headless CLI runner for Postman collections. Newman executes a collection JSON file, resolves environment variables, runs test scripts, and reports results — exiting with code 1 on any test failure for CI integration.
supertest
A Node.js HTTP assertion library that wraps any HTTP app (Express, Fastify, Koa) in a temporary test server, enabling in-process HTTP requests with chained status code and header assertions without a running server.
Contract test
A test that verifies an API implementation conforms to its documented interface (the contract — usually an OpenAPI spec). Contract tests catch breaking changes — renamed fields, type changes — before they affect API consumers, without requiring the consumer and provider to run together.
Response assertion
A test assertion that checks a specific property of an HTTP response — status code, header value, or JSON body field — against an expected value. Chained response assertions make test failure messages precise and actionable.
Test fixture
A static JSON file (or factory function) that provides consistent, reusable test input data. Fixtures centralize test payloads, enable parametrized testing, and prevent data duplication across test files.
Schema validation
Verifying that a JSON response body conforms to a JSON Schema definition — asserting not just that the request succeeded but that the returned data structure, types, and required fields match the documented API contract.

Frequently asked questions

How do I test a JSON API with Postman?

Open Postman and create a new request pointing at your API endpoint. In the Tests tab, write JavaScript using the pm.test() function: pm.test('status is 200', () => { pm.response.to.have.status(200); }). To assert on JSON response fields, call pm.response.json() to parse the body, then use Chai assertions via pm.expect(): pm.expect(pm.response.json().data.id).to.be.a('number'). Group related requests into a Collection and add collection-level pre-request scripts for shared auth. Export the collection as collection.json to run it headlessly with Newman in CI.

How do I run Postman tests in CI with Newman?

Install Newman: npm install -g newman. Export your Postman collection as collection.json and your environment as environment.json. Run: newman run collection.json -e environment.json --reporters cli,junit --reporter-junit-export results.xml. Newman exits with code 1 if any test fails, making it compatible with any CI system. The JUnit reporter produces XML that most CI dashboards display as a test report. For HTML reports, install newman-reporter-htmlextra.

How do I test JSON responses in Node.js?

Use the supertest library with your Express app. Install: npm install --save-dev supertest. In your test: const res = await request(app).get('/users').expect(200).expect('Content-Type', /json/). Then assert on res.body for parsed JSON: expect(res.body[0]).toHaveProperty('id'). supertest starts a temporary server in-process — no running server needed. Chain .expect() calls for status code and Content-Type before accessing res.body for deeper JSON assertions.

What is contract testing for JSON APIs?

Contract testing verifies that a JSON API conforms to its documented interface — usually an OpenAPI JSON spec — without requiring consumer and provider to be tested together. Dredd reads your OpenAPI JSON, generates HTTP requests from the example values in the schema, sends them to your running server, and asserts that responses match the schema definitions. This catches breaking changes (a renamed field, a type change from string to number) before they affect API consumers. See the JSON OpenAPI guide for OpenAPI JSON structure and schema example fields.

How do I validate JSON response schemas in tests?

Use Ajv inside your test assertions. After receiving a response, compile a JSON Schema and validate the parsed body: const valid = ajv.validate(responseSchema, res.body); expect(valid).toBe(true). Alternatively, use chai-json-schema for a Chai plugin style: expect(res.body).to.be.jsonSchema(responseSchema). Schema validation catches structural regressions — missing fields, wrong types, unexpected nulls — that status code assertions miss. See the JSON Schema testing guide for Ajv error assertion patterns.

How do I test APIs with Playwright?

Playwright has a built-in API testing context. In your test: const response = await request.get('/users/1'); expect(response).toBeOK(). The toBeOK() matcher checks for 2xx status codes. Parse the JSON body with const body = await response.json() and assert with standard Jest-style matchers: expect(body.id).toBe(1). Playwright API tests run without a browser, start faster than browser tests, and integrate with the same test runner as your E2E tests — no separate framework needed.

What is the Postman collection JSON format?

A Postman collection is a JSON file with a top-level info object and an item array. The info object must include a schema field set to https://schema.getpostman.com/json/collection/v2.1.0/collection.json for version 2.1. Each item has name, request (method, url, headers, body), and event (pre-request and test scripts). Items nest inside folder items (which also have an item array) for organization. Environment variable references use {{variableName}} syntax and are resolved at runtime with a -e environment.json file in Newman.

How do I test JSON error responses?

Test error responses the same way as success responses — assert on status code, Content-Type, and JSON body structure. In supertest: const res = await request(app).post('/users').send({}).expect(400).expect('Content-Type', /json/). Then: expect(res.body).toHaveProperty('error'). In Postman: pm.test('400 has error field', () => { pm.expect(pm.response.json()).to.have.property('error'); }). Also validate the error response body against a JSON Schema — error payloads are API contracts too, and consumers rely on a predictable error shape. See the JSON data validation guide for schema validation patterns.

Further reading and primary sources

Format and validate your JSON now

Paste your Postman collection JSON, API response body, or JSON fixture into Jsonic's formatter to validate structure and catch syntax errors instantly.

Open JSON Formatter