# Dataset Contract & Regression Tester (`gifted_wagon/dataset-contract-tester`) Actor

Test Apify datasets against versioned contracts. Catch missing and extra fields, type and nullability drift, duplicate keys, and breaking schema changes with CI-ready evidence.

- **URL**: https://apify.com/gifted\_wagon/dataset-contract-tester.md
- **Developed by:** [Michael Olmos](https://apify.com/gifted_wagon) (community)
- **Categories:** Developer tools, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.40 / 1,000 dataset item validateds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Dataset Contract & Regression Tester

Stop broken dataset changes **before** they break a pipeline.

This Actor infers or enforces a versioned dataset contract, validates every JSON item, detects duplicate composite keys, and compares the current contract with a previous version. It produces compact, evidence-rich item results and a suite result that can gate a deployment, scraper release, ETL job, dashboard, or RAG ingestion workflow.

It is deterministic, requires no external API key, does not browse the web, and does not send your data to an AI model.

### What it catches

- missing required fields
- undeclared extra fields
- string, number, integer, boolean, object, and array type drift
- null values in non-nullable fields
- duplicate single-field or composite keys
- fields added or removed between contract versions
- type, required, and nullability changes
- duplicate-key or extra-field policy changes
- structural contract changes made without a version bump

Each finding identifies the rule, field path, expected state, actual state, and a short explanation. Optional mismatched-value previews are capped at 120 characters. Duplicate key values are represented only by a short SHA-256 fingerprint.

### Quick start

Run with `{}` or no input to use the safe built-in sample. It demonstrates valid records, a missing field, an unexpected field, a type change, null handling, a duplicate key, and schema changes between contract versions.

For a real dataset, select it in **Dataset to test** and provide a contract:

```json
{
  "datasetId": "YOUR_DATASET_ID",
  "contract": {
    "schemaVersion": "dataset-contract-v1",
    "name": "orders",
    "version": "2.0.0",
    "fields": {
      "id": { "type": "string", "required": true, "nullable": false },
      "total": { "type": "number", "required": true, "nullable": false },
      "currency": { "type": "string", "required": true, "nullable": false },
      "paid": { "type": "boolean", "required": true, "nullable": false }
    },
    "keyFields": ["id"],
    "allowExtraFields": false
  },
  "maxItems": 500
}
```

You can paste `items`, choose `datasetId`, or combine both. Inline items are evaluated first. Work stops at `maxItems` or the run's pay-per-event spending limit, whichever comes first.

### Infer a contract

Omit `contract` to infer one from the selected items:

```json
{
  "datasetId": "YOUR_DATASET_ID",
  "contractName": "customer-export",
  "contractVersion": "1.0.0",
  "requiredPresenceRatio": 1,
  "keyFields": ["customerId"],
  "allowExtraFields": true
}
```

Inference observes all items loaded for the run. A field becomes required when its presence ratio reaches `requiredPresenceRatio`. Observed non-null types become its accepted types, and observed nulls make it nullable. If no keys are supplied, the Actor tries a fully present, unique `id`, `_id`, `url`, `email`, or `key` field.

Save the normalized `CONTRACT` output as a reviewed baseline. For stable production and CI checks, pass that explicit contract on later runs; do not rely on fresh inference to catch drift in the same batch.

### Compare contract versions

Pass `baselineContract` alongside the current `contract`. The suite result reports additive and breaking changes with before/after evidence:

```json
{
  "items": [{ "id": "a", "score": 42, "active": true }],
  "contract": {
    "name": "scores",
    "version": "2.0.0",
    "fields": {
      "id": { "type": "string", "required": true },
      "score": { "type": "integer", "required": true },
      "active": { "type": "boolean", "required": true }
    },
    "keyFields": ["id"],
    "allowExtraFields": false
  },
  "baselineContract": {
    "name": "scores",
    "version": "1.0.0",
    "fields": {
      "id": { "type": "string", "required": true },
      "score": { "type": "number", "required": true }
    },
    "keyFields": ["id"],
    "allowExtraFields": false
  }
}
```

The comparison is semantic rather than textual. Field ordering does not matter.

### Output

The default dataset contains:

1. One `recordType: "item"` result for each evaluated item.
2. One uncharged `recordType: "suite"` result with totals and schema changes.

The `OUTPUT` key-value-store record contains the same suite metrics plus source information, the effective contract, run timestamps, and a budget-limit flag. `CONTRACT` contains only the normalized effective contract.

A failing contract check is still a successful evaluation:

```json
{
  "recordType": "item",
  "itemIndex": 2,
  "itemKey": "sha256:70cd1f58c55d65cb",
  "status": "failed",
  "violationCount": 2,
  "violations": [
    {
      "rule": "type-mismatch",
      "path": "score",
      "expected": "integer",
      "actual": "string",
      "valuePreview": "unknown",
      "evidence": "Field \"score\" has type string; expected integer."
    }
  ],
  "valueEvent": "item-validated"
}
```

Use the suite `status`, `itemsFailed`, `processingErrors`, `totalViolations`, or error-severity `schemaChanges` as CI gates.

### Pricing and charging

Contract violations are successful paid evaluations because the finding is the product. Internal processing failures are returned with `valueEvent: null` and are uncharged. The suite dataset record, `OUTPUT` summary, effective `CONTRACT`, input validation, and schema comparison are also uncharged.

| Apify tier | Price per evaluated item |
|---|---:|
| Free | $0.0020 |
| Bronze | $0.0018 |
| Silver | $0.0016 |
| Gold | $0.0014 |
| Platinum and Diamond | $0.0012 |

A one-time `$0.0003` Actor-start event covers measured startup overhead, including runs stopped by invalid input. A 500-item Free-tier test has a maximum event price of `$1.0003`. The Actor calculates the allowed value-event count before processing and sets `limitedByBudget: true` when a run limit truncates work.

### Privacy and permissions

- Limited permissions: read only the dataset you select; write only the run's default output dataset and key-value store.
- No web requests, proxies, cookies, credentials, tracking pixels, or external analytics.
- No external AI or third-party enrichment service.
- Original source rows are never copied to output.
- Only violating values can produce a preview, capped at 120 characters; set `includeValueEvidence: false` to suppress all previews.
- Composite duplicate keys are hashed before output and logs never include item contents.
- Input and output remain subject to your Apify account's storage and retention settings.

If your dataset contains private, regulated, or customer data, disable value evidence, use appropriate Apify storage retention, and confirm your organization's data-handling requirements before running it.

### Important limitations

- Contracts validate top-level fields. Nested objects and arrays are type-checked as objects or arrays, but nested paths and array element schemas are not recursively validated in version 0.1.
- A `number` contract accepts integers; an `integer` contract rejects non-integer numbers.
- Inference describes the loaded sample, not the unseen source population. Increase `maxItems`, use representative data, and review the inferred contract before adopting it.
- Duplicate detection is exact after JSON serialization of configured key values. It does not fuzzy-match names, normalize emails, or merge near-duplicates.
- Structural validity does not prove that values are current, truthful, legally usable, or semantically correct.

### API and automation

```bash
curl -X POST \
  "https://api.apify.com/v2/acts/gifted_wagon~dataset-contract-tester/runs" \
  -H "Authorization: Bearer $APIFY_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"datasetId":"YOUR_DATASET_ID","contractName":"nightly-export","contractVersion":"1.0.0"}'
```

Run it after a scraper, on a schedule, from GitHub Actions, or through Apify webhooks, Make, Zapier, n8n, an API client, or the hosted Apify MCP server.

### Support

Open an issue from the Actor's **Issues** tab and include the run ID, contract version, redacted contract, and redacted violation. Never post private rows, credentials, access tokens, or unhashed personal identifiers in a public issue.

# Actor input Schema

## `datasetId` (type: `string`):

Optional Apify dataset. Its items are read once up to Maximum items.

## `items` (type: `array`):

JSON objects to test. Inline and dataset items can be combined.

## `contract` (type: `object`):

Optional dataset-contract-v1 object. Define fields with type or types, required, nullable, keyFields, version, and allowExtraFields. Leave empty to infer it.

## `baselineContract` (type: `object`):

Optional prior dataset-contract-v1 object used to report field, type, required, nullability, key, and policy changes.

## `contractName` (type: `string`):

Used only when the current contract is inferred.

## `contractVersion` (type: `string`):

Version attached to an inferred contract.

## `keyFields` (type: `array`):

Top-level fields forming a composite key when the current contract is inferred. If empty, a unique id, \_id, url, email, or key may be inferred.

## `allowExtraFields` (type: `boolean`):

If false, fields outside the inferred field set are reported as warnings on later validation input.

## `requiredPresenceRatio` (type: `number`):

A field observed in at least this fraction of sampled items becomes required. Use 1 for strict inference.

## `maxItems` (type: `integer`):

Hard work limit across inline and dataset items. Pay-per-event budget limits can reduce it further.

## `maxViolationsPerItem` (type: `integer`):

Caps evidence output for unusually wide or malformed items.

## `includeValueEvidence` (type: `boolean`):

Include at most 120 characters of a mismatched or extra value. Disable for sensitive datasets; duplicate keys are always hashed.

## Actor input object example

```json
{
  "items": [
    {
      "id": "order-001",
      "total": 28.5,
      "currency": "USD",
      "paid": true
    },
    {
      "id": "order-002",
      "total": 19,
      "currency": "USD",
      "paid": false
    }
  ],
  "contractName": "dataset-contract",
  "contractVersion": "1.0.0",
  "keyFields": [],
  "allowExtraFields": true,
  "requiredPresenceRatio": 1,
  "maxItems": 500,
  "maxViolationsPerItem": 50,
  "includeValueEvidence": true
}
```

# Actor output Schema

## `dataset` (type: `string`):

One charged validation record per evaluated item, followed by one uncharged suite record.

## `summary` (type: `string`):

Pass/fail totals, violation counts, schema changes, budget status, and effective contract.

## `contract` (type: `string`):

The normalized provided contract or the inferred contract used by this run.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "items": [
        {
            "id": "order-001",
            "total": 28.5,
            "currency": "USD",
            "paid": true
        },
        {
            "id": "order-002",
            "total": 19,
            "currency": "USD",
            "paid": false
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("gifted_wagon/dataset-contract-tester").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "items": [
        {
            "id": "order-001",
            "total": 28.5,
            "currency": "USD",
            "paid": True,
        },
        {
            "id": "order-002",
            "total": 19,
            "currency": "USD",
            "paid": False,
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("gifted_wagon/dataset-contract-tester").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "items": [
    {
      "id": "order-001",
      "total": 28.5,
      "currency": "USD",
      "paid": true
    },
    {
      "id": "order-002",
      "total": 19,
      "currency": "USD",
      "paid": false
    }
  ]
}' |
apify call gifted_wagon/dataset-contract-tester --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,gifted_wagon/dataset-contract-tester"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/8Zv2uQUbI7A08qaWV/builds/1OqbkWLIbyZGgktnn/openapi.json
