ScreenshotNeo

BlogHow-to

How to Validate JavaScript Data with Cypress

Validate Cypress API responses and JavaScript objects with precise Chai assertions, retries, fixtures, error checks, and practical troubleshooting.

By the ScreenshotNeo team30 September 202611 min read

How to Validate JavaScript Data with Cypress

Use Cypress’s built-in Chai assertions to validate JavaScript data. For API responses, call cy.request(), inspect its status, headers, duration, and body, then assert the exact contract your application depends on. Use expect() inside a .then() callback for resolved data, or .should() when Cypress should retry a changing subject.

Cypress bundles Chai and adds assertion extensions, so you can check properties, keys, types, values, arrays, and deep equality without installing another assertion library. See the official Cypress assertions reference.

1. The basic patterns

Validate an object returned by an API

cy.request('/cart').its('body').then((cart) => {
  expect(cart).to.have.all.keys(
    'id', 'items', 'subtotal', 'tax', 'total', 'currency'
  )
  expect(cart.currency).to.be.oneOf(['USD', 'EUR', 'GBP'])
  expect(cart.total).to.be.a('number')

  cart.items.forEach((item) => {
    expect(item).to.include.all.keys('sku', 'quantity', 'unitPrice')
    expect(item.quantity).to.be.greaterThan(0)
  })
})

This checks the contract and the values that matter to the consumer. all.keys also fails when an unexpected key appears. If additive fields are allowed, use partial key assertions instead.

Check one property

cy.request('/users/1')
  .its('body.username')
  .should('eq', 'jdoe')

Compare a complete object

cy.request('/users/1')
  .its('body')
  .should('deep.eq', { name: 'Jane', username: 'jdoe' })

Deep equality compares nested values and types. It is useful for a deliberately fixed response, but it can make a test fail whenever the server adds an unrelated field.

2. A complete Cypress example

The following spec covers a success response, nested data, an expected validation error, and a fixture. Replace the URLs and contract details with those used by your application.

A Cypress response can be checked at the status, shape, type, and value levels.
A Cypress response can be checked at the status, shape, type, and value levels.
describe('order API data', () => {
  it('validates the successful response shape and values', () => {
    cy.request({
      method: 'GET',
      url: '/api/orders/123',
    }).then((response) => {
      expect(response.status).to.eq(200)
      expect(response.headers).to.have.property('content-type')
      expect(response.duration).to.be.a('number')

      const order = response.body
      expect(order).to.have.all.keys(
        'id', 'lineItems', 'subtotal', 'tax', 'total', 'currency'
      )
      expect(order.id).to.eq('123')
      expect(order.currency).to.be.oneOf(['USD', 'EUR', 'GBP'])
      expect(order.subtotal).to.be.a('number')
      expect(order.tax).to.be.a('number')
      expect(order.total).to.be.a('number')
      expect(order.lineItems).to.be.an('array').and.not.be.empty

      order.lineItems.forEach((lineItem) => {
        expect(lineItem).to.include.all.keys(
          'sku', 'quantity', 'unitPrice'
        )
        expect(lineItem.sku).to.be.a('string').and.not.be.empty
        expect(lineItem.quantity).to.be.a('number')
        expect(lineItem.quantity).to.be.greaterThan(0)
        expect(lineItem.unitPrice).to.be.a('number')
        expect(lineItem.unitPrice).to.be.at.least(0)
      })
    })
  })

  it('checks the server-side validation contract', () => {
    cy.request({
      method: 'POST',
      url: '/api/orders',
      body: { lineItems: [] },
      failOnStatusCode: false,
    }).then((response) => {
      expect(response.status).to.eq(422)
      expect(response.body).to.have.property('errors')
      expect(response.body.errors).to.deep.include({
        field: 'lineItems',
        message: 'must contain at least one item',
      })
    })
  })
})

The 422 status and error shape above are example contract values. Keep the assertion aligned with your server’s documented response.

3. Choosing the right Chai assertion

Need Useful assertion What it verifies
One exact value expect(value).to.eq(expected) Strict equality for a scalar
Nested object equality expect(value).to.deep.eq(expected) Recursive value and type equality
Required property expect(object).to.have.property('id') The property exists
Property value expect(object).to.have.property('status', 'paid') Property exists with the expected value
Required keys only expect(object).to.have.all.keys('id', 'status') Exact key set; extra keys fail
Required keys among others expect(object).to.include.all.keys('id', 'status') Required keys; unrelated keys are allowed
Type expect(value).to.be.a('number') Runtime JavaScript type
Allowed values expect(value).to.be.oneOf([...]) Membership in an allowed set
Array contents expect(array).to.deep.include(item) An item appears by nested value
Numeric range expect(value).to.be.greaterThan(0) A boundary or ordering rule

Assert the smallest meaningful contract. A complete deep comparison is appropriate for a stable, versioned payload. Property, key, type, and range assertions are more resilient when the API intentionally permits additional fields.

4. .should() versus .then()

.should() is Cypress’s retryable assertion command when the subject supports retrying. Cypress repeatedly evaluates the assertion until it passes or the command times out. A callback form groups related checks:

cy.get('[data-cy=total]').should(($total) => {
  expect($total).to.have.length(1)
  expect($total).to.contain('$42.00')
})

This is useful when the UI updates after a request. Cypress retries the callback as a unit while the subject is retryable. See the Cypress introduction and retry model.

Use .then() after a resolved request when you want ordinary synchronous JavaScript assertions:

cy.request('/api/profile').then((response) => {
  expect(response.status).to.eq(200)
  expect(response.body.email).to.match(/@/)
})

An assertion chained from cy.request() runs against that resolved response; it does not automatically issue the HTTP request again when the body assertion fails. Request retry options and assertion retries are separate concerns.

5. Validating HTTP responses with cy.request()

cy.request() yields a response containing status, body, headers, and duration. Cypress parses the body as a JavaScript object when the response Content-Type ends in json; otherwise the body is a string. Check the server header when a body unexpectedly has the wrong type.

cy.request({
  method: 'GET',
  url: '/api/catalog',
  headers: {
    Accept: 'application/json',
  },
}).then((response) => {
  expect(response.status).to.eq(200)
  expect(response.headers['content-type']).to.match(/json/)
  expect(response.body).to.be.an('array')
})

For authentication, pass the headers or cookies required by the endpoint and assert the authenticated contract rather than merely checking status:

cy.request({
  method: 'GET',
  url: '/api/me',
  headers: {
    Authorization: `Bearer ${Cypress.env('API_TOKEN')}`,
  },
}).then(({ status, body }) => {
  expect(status).to.eq(200)
  expect(body).to.include.all.keys('id', 'email', 'roles')
  expect(body.roles).to.be.an('array')
})

6. Testing expected failures

By default, Cypress fails a request when the server returns a non-2xx or non-3xx status. Set failOnStatusCode: false when the purpose of the test is to inspect that error response.

cy.request({
  method: 'POST',
  url: '/api/users',
  body: { email: 'not-an-email' },
  failOnStatusCode: false,
}).then((response) => {
  expect(response.status).to.eq(400)
  expect(response.body).to.include.all.keys('code', 'message')
  expect(response.body.code).to.eq('INVALID_EMAIL')
})

Do not assert only that a request failed. Assert the status, error code, field, and message your client uses. This catches regressions where the server returns the wrong error for the right input.

7. Avoiding weak negative assertions

A negative assertion can pass for an unintended reason. For example, checking that a list is not a particular length may pass because the application deleted every item, inserted a blank row, or failed to render the response. Prefer the expected shape and count:

// Weak: several incorrect states can satisfy this.
cy.get('[data-cy=results]').should('not.have.length', 3)

// Stronger: state the intended result directly.
cy.get('[data-cy=result]').should('have.length', 2)
cy.get('[data-cy=result]').each(($result) => {
  expect($result).to.have.attr('data-id').and.not.be.empty
})

For API data, assert the expected array length, required keys, and meaningful values. Use negative checks only when the absence itself is the contract.

8. Fixtures and static JavaScript data

Keep a small, test-specific object inline when it clarifies the scenario. Put shared or substantial data in a fixture file. Cypress documents JSON and JavaScript fixtures in the fixture API reference.

// cypress/fixtures/order.json
{
  "id": "fixture-order-1",
  "currency": "USD",
  "lineItems": [
    { "sku": "sku-1", "quantity": 2, "unitPrice": 10 }
  ]
}
cy.fixture('order').then((expectedOrder) => {
  expect(expectedOrder).to.include.all.keys('id', 'currency', 'lineItems')
  expect(expectedOrder.lineItems).to.be.an('array').and.not.be.empty
})

Fixtures are loaded data, not a schema validator. Keep their assertions representative of the contract and update both together when the contract intentionally changes.

9. Validating transformed data

When application code maps an API response into a view model, validate both the source and the transformation boundary. This prevents a test from passing because the UI hides a missing field.

cy.request('/api/products').then(({ body }) => {
  expect(body).to.be.an('array')

  const cards = body.map((product) => ({
    id: product.id,
    title: product.name,
    priceLabel: `$${product.price.toFixed(2)}`,
  }))

  cards.forEach((card) => {
    expect(card).to.have.all.keys('id', 'title', 'priceLabel')
    expect(card.id).to.be.a('string').and.not.be.empty
    expect(card.title).to.be.a('string').and.not.be.empty
    expect(card.priceLabel).to.match(/^\$\d+\.\d{2}$/)
  })
})

For optional fields, assert the allowed alternatives explicitly:

expect(user.displayName == null || typeof user.displayName === 'string')
  .to.eq(true)

10. Equivalent checks with cURL, Python, and Node.js

Cypress is useful when the check belongs in a browser test suite. For a quick contract probe in a shell or service, the same response can be inspected with other clients.

cURL

curl --fail-with-body -sS https://example.test/api/users/1 \
  -H 'Accept: application/json' \
  -o response.json
cat response.json

Python

import requests

response = requests.get(
    'https://example.test/api/users/1',
    headers={'Accept': 'application/json'},
    timeout=30,
)
response.raise_for_status()
data = response.json()
assert isinstance(data['username'], str)
assert data['username'] == 'jdoe'

Node.js

const response = await fetch('https://example.test/api/users/1', {
  headers: { Accept: 'application/json' },
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
if (data.username !== 'jdoe') {
  throw new Error(`Unexpected username: ${data.username}`);
}

11. Troubleshooting common failures

Symptom Likely cause Fix
response.body is a string The response Content-Type does not end in json. Fix the server header, or parse the string deliberately after asserting its format.
The test fails before inspecting an error body A non-2xx/3xx response triggered the default failure. Set failOnStatusCode: false for deliberately invalid requests.
An assertion never retries the request Assertions chained from cy.request() run once. Use request retry options for transport problems, or poll through a retryable Cypress subject when the data changes asynchronously.
all.keys fails after a harmless API change The assertion requires an exact key set. Use include.all.keys when extra fields are allowed, or update the versioned contract.
Deep equality fails on equivalent-looking data Types, missing keys, array order, or nested values differ. Inspect the actual object and assert the specific contract dimensions that matter.
A negative UI assertion passes unexpectedly The application reached a different empty or malformed state. Assert the expected count, keys, identifiers, and values directly.
Authentication returns 401 or 403 The token, cookie, scope, or environment is missing or expired. Pass the required headers/cookies, verify the test environment, and assert the intended authenticated response.
Intermittent timeout in a UI assertion The subject updates after asynchronous work or the timeout is too short. Use .should() on a retryable subject, wait on a meaningful application condition, and change timeouts only when the operation genuinely needs longer.

12. Performance and reliability

  • Validate the response at the API boundary when the goal is data correctness; this avoids waiting for rendering and produces a more focused failure.
  • Keep assertions specific. Checking every incidental field increases maintenance and makes harmless additive changes break tests.
  • Use one request when several assertions describe the same response. Store response.body in the callback rather than issuing duplicate requests.
  • Use deterministic fixtures or controlled test data for stable contracts. Avoid depending on mutable production records.
  • Record response.duration when latency is diagnostically useful, but do not turn an incidental local duration into a universal performance budget.
  • Separate transport retries from assertion retries. A body assertion failure does not imply a new HTTP request.
  • For eventual consistency, poll through an application-facing retryable command or explicitly repeat the request with a bounded strategy rather than hiding a race with a long arbitrary delay.

13. Cost and test-suite maintenance

Cypress itself does not charge per assertion. The practical costs are CI time, test-environment capacity, and maintenance when contracts are over-specified. Exact-key and deep-equality checks provide stronger change detection but require deliberate versioning. Partial assertions reduce churn while still protecting the fields the client uses.

Keep expensive setup outside individual assertions, reuse a resolved response for related checks, and reserve end-to-end browser flows for behavior that needs a browser. API contract checks can run faster and explain failures more directly.

14. Or skip the browser setup

If the goal is to capture a clean screenshot of a Cypress result, test report, or application state for review, ScreenshotNeo provides a single HTTP request. Its API can return PNG, JPEG, WebP, or PDF, and the ScreenshotNeo documentation lists the options.

ScreenshotNeo removes common overlays before capture so test evidence stays readable.
ScreenshotNeo removes common overlays before capture so test evidence stays readable.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' }); const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Before capture, ScreenshotNeo accepts cookie or consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits are not billed, and the response reports the result with X-Page-Verdict and X-Billed headers. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients. The Free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 shots.

Create a free ScreenshotNeo account and get 1,000 screenshots a month without a card.

15. FAQ

Should I validate status before the body?

Yes. Assert the expected status first, then validate the body contract for that status. For expected failures, disable automatic status failure so the body can be inspected.

Is deep.eq better than property assertions?

Neither is universally better. Use deep equality for a stable complete object; use property, key, type, and value assertions when unrelated fields may change.

Why did Cypress not retry my API request?

Assertions on a resolved cy.request() response run once. Cypress assertion retries apply to retryable subjects; request retries are configured separately.

Can Cypress validate non-JSON responses?

Yes. Cypress yields non-JSON bodies as strings. Assert the content type and parse or match the string according to the response contract.

Where should shared expected data live?

Use a fixture for substantial or shared data and inline objects for small, scenario-specific values. Keep the assertions tied to the contract rather than treating a fixture as a schema.

16. Practical checklist

  • Assert the expected HTTP status.
  • Confirm the response body type and content type.
  • Choose exact keys or partial keys intentionally.
  • Check required properties, types, ranges, and allowed values.
  • Validate nested arrays item by item when their shape matters.
  • Use failOnStatusCode: false for expected error responses.
  • Use .should() for retryable changing subjects and .then() for resolved data.
  • Avoid negative assertions that can pass for unrelated states.
  • Keep fixture data representative and deterministic.
  • Separate API transport reliability from assertion behavior.