# Structured Data Validator: Schema Markup Checker (`maliksaid/structured-data-validator`) Actor

Schema markup validator for your pages. Checks JSON-LD, Microdata and RDFa against schema.org and the Google rich result rules for Product, Article, Event, Recipe, Local business and more. One row per URL with types, errors and warnings. Reads URLs or sitemaps, with a change-only mode.

- **URL**: https://apify.com/maliksaid/structured-data-validator.md
- **Developed by:** [Malik Said](https://apify.com/maliksaid) (community)
- **Categories:** SEO tools, Developer tools
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.00 / 1,000 page checkeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
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.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## 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.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Structured Data Validator: Schema Markup Checker

This structured data validator checks the schema markup on your web pages. It reads JSON-LD, Microdata and RDFa, validates every item against the schema.org vocabulary, and checks the required and recommended properties of Google rich results. You get one row per URL with the types found, errors, warnings and which rich results are valid. Give it page URLs, an XML sitemap, or let it follow links on your site. A change-only mode tells you when a page's markup breaks.

It is built for site owners, SEO consultants, agencies and developers who test their own or their clients' sites in bulk.

### What the schema markup validator checks

| Area | What you get |
|---|---|
| Formats | JSON-LD (including `@graph` and `@id` references), HTML Microdata (including `itemref`) and schema.org RDFa Lite |
| JSON-LD syntax | Blocks that are not valid JSON, empty blocks, a missing or non-schema.org `@context` |
| schema.org types | Unknown types, wrong letter case (for example `product`), superseded types, items with no type |
| schema.org properties | Unknown properties, properties schema.org does not list for the type, superseded properties |
| Values | Dates and times (ISO 8601), durations (`PT1H30M`), prices with currency signs, currency codes, numbers in ratings, enumeration values such as `availability`, URLs, empty values |
| Rich result rules | Required and recommended properties for 19 Google rich result features (list below) |
| Retired features | FAQ and How-to markup is flagged, because Google no longer shows these rich results |

#### Rich result features covered

Product snippet, Merchant listing, Review snippet, Article (also NewsArticle and BlogPosting), Breadcrumb, Event, Recipe, Job posting, Local business (and subtypes such as Restaurant), Organization, Video, Software app, Course, Dataset, Discussion forum, Movie, Q\&A, Profile page and Image metadata.

The rules were written by hand from Google Search Central's public structured data pages, read on 21 October 2026. The Actor does not call any Google API. Its results can differ from Google's Rich Results Test. Google decides what it shows in search.

### Structured data validator output

One dataset row per page:

- `url`, `finalUrl`, `httpStatus`, `contentType`
- `jsonLdCount`, `microdataCount`, `rdfaCount`, `itemCount`, `hasStructuredData`
- `types`: the schema.org types of the top-level items
- `richResults`: one entry per rich result feature found, with `valid`, `errorCount` and `warningCount`
- `validRichResults`: features with at least one item that has no errors
- `errorCount`, `warningCount`, `errorCodes`, `issueCounts`
- `errors` and `warnings`: up to 200 each, with `code`, `format`, `itemIndex`, `itemType`, `path` (for example `offers.price`) and a plain message
- `items`: the raw items as JSON, when **Include raw items** is on
- `changeStatus`, `previousErrorCount`, `previousTypes`: in change-only mode
- `fetchError`: why a page could not be checked

Errors block a rich result. Warnings are missing recommended properties and schema.org notes that do not block it.

#### Error and warning codes

| Code | Severity | Meaning |
|---|---|---|
| `JSONLD_PARSE_ERROR` | Error | A JSON-LD block is not valid JSON |
| `JSONLD_MISSING_CONTEXT` | Error | A JSON-LD item has no `@context` |
| `JSONLD_NON_SCHEMA_CONTEXT` | Warning | The `@context` is not schema.org |
| `MISSING_TYPE` | Error | A top-level item has no type |
| `UNKNOWN_TYPE` | Error | The type is not in schema.org |
| `UNKNOWN_PROPERTY` | Error | The property is not in schema.org |
| `MISSING_REQUIRED` | Error | A required rich result property is missing |
| `INVALID_DATE` | Error | A date is not ISO 8601 |
| `INVALID_DURATION` | Error | A duration is not ISO 8601 |
| `INVALID_PRICE` | Error | A price is not a plain number |
| `INVALID_CURRENCY` | Error | A currency is not a 3-letter ISO 4217 code |
| `INVALID_NUMBER` | Error | A rating or count is not a number |
| `MISSING_RECOMMENDED` | Warning | A recommended rich result property is missing |
| `PROPERTY_NOT_FOR_TYPE` | Warning | schema.org does not list the property for this type |
| `INVALID_ENUM_VALUE` | Warning | The value is not a member of the expected list, for example `availability` |
| `INVALID_URL` | Warning | A URL property holds text with spaces |
| `EMPTY_VALUE` | Warning | A property is empty |
| `DEPRECATED_TYPE`, `DEPRECATED_PROPERTY` | Warning | schema.org marks the name as superseded |
| `NON_SCHEMA_ORG_TYPE` | Warning | The type is from another vocabulary and was not validated |
| `RICH_RESULT_RETIRED` | Warning | FAQ or How-to markup that Google no longer shows |

#### Run summary

The key-value store record `OUTPUT` holds the run summary: pages checked, pages with structured data, pages with errors, pages without markup, items by format, pages by type, valid and invalid counts per rich result feature, error and warning code counts, change counts, pages skipped by robots.txt and the sitemap result.

### How to validate structured data in bulk

1. Enter your page URLs in **Page URLs**. Or enter your sitemap in **Sitemap URLs**, or turn on **Find sitemaps in robots.txt**.
2. Turn on **Follow links on the same site** to check pages you did not list.
3. Set **Maximum pages**. The default is 100. Set 0 for no limit.
4. Run the Actor. Open the **Overview** view for one row per page, or the **Errors** view for one row per error.

#### Change-only mode

Turn on **Change-only mode** and schedule the Actor, for example daily. The first run saves every page with `changeStatus: NEW`. Later runs save only pages whose result changed: `BROKE` (more errors), `FIXED` (fewer errors), `MARKUP_REMOVED`, `CHANGED` or `FETCH_FAILED`. Use a different **Change-only state key** for each site. In a local test on 20 blog pages, the second run saved 0 rows.

### Input example

```json
{
    "startUrls": [{ "url": "https://webscraper.io/test-sites/e-commerce/allinone" }],
    "sitemapUrls": [],
    "crawlLinks": false,
    "includeItems": false,
    "changeOnly": false,
    "maxItems": 100
}
```

### Output example

Real row for the webscraper.io test page, from a run on 22 October 2026. The `richResults`, `errors` and `warnings` lists are shortened to 2 entries each.

```json
{
    "id": "https://webscraper.io/test-sites/e-commerce/allinone",
    "url": "https://webscraper.io/test-sites/e-commerce/allinone",
    "finalUrl": "https://webscraper.io/test-sites/e-commerce/allinone",
    "httpStatus": 200,
    "contentType": "text/html; charset=utf-8",
    "jsonLdCount": 0,
    "microdataCount": 4,
    "rdfaCount": 0,
    "itemCount": 4,
    "hasStructuredData": true,
    "types": [
        "SiteNavigationElement",
        "Product"
    ],
    "richResults": [
        {
            "feature": "Product snippet",
            "itemIndex": 1,
            "itemType": "Product",
            "format": "microdata",
            "valid": false,
            "errorCount": 2,
            "warningCount": 4
        },
        {
            "feature": "Merchant listing",
            "itemIndex": 1,
            "itemType": "Product",
            "format": "microdata",
            "valid": false,
            "errorCount": 1,
            "warningCount": 8
        }
    ],
    "validRichResults": [],
    "errorCount": 6,
    "warningCount": 36,
    "errors": [
        {
            "severity": "error",
            "code": "INVALID_PRICE",
            "format": "microdata",
            "itemIndex": 1,
            "itemType": "Product",
            "path": "offers.price",
            "message": "Price \"$1769\" must be a number, for example 1179.00, with no currency sign. Put the currency in priceCurrency."
        },
        {
            "severity": "error",
            "code": "MISSING_REQUIRED",
            "format": "microdata",
            "itemIndex": 1,
            "itemType": "Product",
            "path": "aggregateRating.ratingValue",
            "message": "Product snippet: required property \"aggregateRating.ratingValue\" is missing."
        }
    ],
    "warnings": [
        {
            "severity": "warning",
            "code": "MISSING_RECOMMENDED",
            "format": "microdata",
            "itemIndex": 1,
            "itemType": "Product",
            "path": "review",
            "message": "Product snippet: recommended property \"review\" is missing."
        },
        {
            "severity": "warning",
            "code": "MISSING_RECOMMENDED",
            "format": "microdata",
            "itemIndex": 1,
            "itemType": "Product",
            "path": "offers.availability",
            "message": "Product snippet: recommended property \"offers.availability\" is missing."
        }
    ],
    "errorCodes": [
        "INVALID_PRICE",
        "MISSING_REQUIRED"
    ],
    "issueCounts": {
        "INVALID_PRICE": 3,
        "MISSING_REQUIRED": 3,
        "MISSING_RECOMMENDED": 36
    },
    "changeStatus": null,
    "previousErrorCount": null,
    "previousTypes": null,
    "items": null,
    "fetchError": null,
    "scrapedAt": "2026-10-22T08:19:36.521Z"
}
```

### Real findings from test runs

- On the webscraper.io e-commerce test page, every product's Microdata price includes a dollar sign, for example `$1102.66`. Each one is an `INVALID_PRICE` error. The `AggregateRating` items have `reviewCount` but no `ratingValue`, so each product also gets `MISSING_REQUIRED` at `aggregateRating.ratingValue`.
- On a public blog, one article's second JSON-LD block had a raw line break inside a string. The row showed `JSONLD_PARSE_ERROR`. Python's JSON parser confirmed the same error.
- On the same blog, articles with an extra `FAQPage` item got `RICH_RESULT_RETIRED`.
- On another site's blog, `datePublished` was written as `August 24, 2026`. Each post got `INVALID_DATE`.
- A local full crawl of a test site with **Follow links on the same site** checked 534 pages in about 7 minutes at 120 requests per minute. Linked CSV, JSON and Excel downloads were skipped without a row or a charge.

### Pricing

Pay per event:

- $0.02 per run start.
- $2.00 per 1,000 pages checked ($0.002 per page).

A run of 500 pages costs $1.02. In change-only mode every checked page is charged, saved or not. You can set a maximum cost per run in the run options.

### Polite crawling and limits

- The Actor obeys robots.txt for every page and sitemap. Skipped pages are listed in the summary.
- It loads at most 60 pages per minute from one site by default. Change **Requests per minute per site** only for your own servers.
- Each page has a 30-second load limit and 2 retries.
- It reads server HTML only. Markup that JavaScript adds after the page loads is not seen.
- Sitemap reading stops after 50 files or 2 minutes.
- Check only sites you own or have permission to audit.

### FAQ

**Is this the same as Google's Rich Results Test?** No. It applies its own rules, written from Google's public documentation, and the schema.org vocabulary. Use it to find problems across many pages. Confirm key pages in Google's tools.

**Why does a Microdata `name` on a link show a URL?** The HTML Microdata standard reads the `href` of an `<a>` element as the property value. Put `itemprop="name"` on a `<span>` inside the link to give the text.

**Which schema.org version is used?** The current schema.org release, fetched on 21 October 2026. schema.org data is licensed under CC BY-SA 3.0.

**Does it render JavaScript?** No. Most sites put JSON-LD in the server HTML. If yours adds it with JavaScript, the page shows `hasStructuredData: false`.

### Related Actors

- [Technical SEO Audit & Broken Link Checker](https://apify.com/maliksaid/technical-seo-audit): titles, canonicals, redirects, broken links and an SEO score per page.
- [Sitemap Health Audit](https://apify.com/maliksaid/sitemap-health-audit): checks every URL in your XML sitemaps for status, redirects, canonicals and noindex.

### Support

Open an issue on the Actor's Issues tab. Include the run ID and the page URL.

# Actor input Schema

## `startUrls` (type: `array`):

Enter the pages to check. Each page gets one result row. Check only sites you own or have permission to audit.

## `sitemapUrls` (type: `array`):

Enter XML sitemap URLs. Every page listed in them is checked, up to the page limit. Sitemap index files are followed.

## `useSitemaps` (type: `boolean`):

Read robots.txt of each page URL's site and check the pages in the sitemaps it declares. Uses /sitemap.xml when robots.txt declares none.

## `crawlLinks` (type: `boolean`):

Add same-site links found on checked pages, until the page limit is reached.

## `includeItems` (type: `boolean`):

Add the extracted JSON-LD, Microdata and RDFa items to each row, in JSON form. Rows get larger.

## `changeOnly` (type: `boolean`):

Save only pages whose markup result changed since the last run with the same state key: new, broke, fixed, changed or markup removed. Schedule the Actor with this on to watch your markup. Every checked page is charged, saved or not.

## `stateKey` (type: `string`):

Name of the memory that change-only mode uses. Use a different key for each site you watch.

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

Stop after checking this many pages. The default keeps test runs short and cheap. Set 0 for no limit.

## `maxRequestsPerMinute` (type: `integer`):

Limit how fast the Actor loads pages from one site. Raise it only for your own servers.

## `maxConcurrency` (type: `integer`):

Set how many pages load at the same time.

## `proxyConfiguration` (type: `object`):

Choose a proxy if your site blocks data-center IP addresses. Most sites need none.

## Actor input object example

```json
{
  "startUrls": [
    {
      "url": "https://webscraper.io/test-sites/e-commerce/allinone"
    },
    {
      "url": "https://webscraper.io/test-sites/e-commerce/allinone/computers/laptops"
    },
    {
      "url": "https://webscraper.io/test-sites/e-commerce/allinone/phones/touch"
    }
  ],
  "sitemapUrls": [],
  "useSitemaps": false,
  "crawlLinks": false,
  "includeItems": false,
  "changeOnly": false,
  "stateKey": "default",
  "maxItems": 100,
  "maxRequestsPerMinute": 60,
  "maxConcurrency": 3,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `pages` (type: `string`):

No description

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

No description

# 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 = {
    "startUrls": [
        {
            "url": "https://webscraper.io/test-sites/e-commerce/allinone"
        },
        {
            "url": "https://webscraper.io/test-sites/e-commerce/allinone/computers/laptops"
        },
        {
            "url": "https://webscraper.io/test-sites/e-commerce/allinone/phones/touch"
        }
    ],
    "maxItems": 100,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("maliksaid/structured-data-validator").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 = {
    "startUrls": [
        { "url": "https://webscraper.io/test-sites/e-commerce/allinone" },
        { "url": "https://webscraper.io/test-sites/e-commerce/allinone/computers/laptops" },
        { "url": "https://webscraper.io/test-sites/e-commerce/allinone/phones/touch" },
    ],
    "maxItems": 100,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("maliksaid/structured-data-validator").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 '{
  "startUrls": [
    {
      "url": "https://webscraper.io/test-sites/e-commerce/allinone"
    },
    {
      "url": "https://webscraper.io/test-sites/e-commerce/allinone/computers/laptops"
    },
    {
      "url": "https://webscraper.io/test-sites/e-commerce/allinone/phones/touch"
    }
  ],
  "maxItems": 100,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call maliksaid/structured-data-validator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,maliksaid/structured-data-validator"
        }
    }
}
```

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/JufUDjsrw1DmQsR2X/builds/ChFo3zfC9lmTPeaFn/openapi.json
