Schema Markup Validator & Generator – JSON-LD Checker avatar

Schema Markup Validator & Generator – JSON-LD Checker

Pricing

from $0.40 / 1,000 result delivereds

Go to Apify Store
Schema Markup Validator & Generator – JSON-LD Checker

Schema Markup Validator & Generator – JSON-LD Checker

Schema markup validator for lists of pages: finds the schema.org JSON-LD on each URL and reports, type by type, the required fields that are missing and the recommended ones. Can also write suggested Organization or LocalBusiness markup for each page.

Pricing

from $0.40 / 1,000 result delivereds

Rating

0.0

(0)

Developer

Locomint

Locomint

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

Share

This schema markup validator reads a list of page URLs and reports, for every schema.org JSON-LD object on each page, which required fields are missing (errors) and which recommended ones are missing (warnings). It can also write suggested Organization or LocalBusiness markup for each page from what the page says about itself, and check that too. $0.50 per 1,000 URLs.

What it does

For every URL you give it, the actor checks the site's robots.txt, fetches the page over plain HTTP (no browser), finds each <script type="application/ld+json"> block and walks every object in it, including @graph lists and nested objects. An Article's publisher is held to the Organization rules just as a top-level Organization is. Each page produces one dataset row.

What the validator reports

FieldWhat goes in it
has_markup, blocksWhether the page has JSON-LD, and how many blocks.
typesEvery @type declared on the page.
validtrue when at least one object was checked and nothing is wrong; false when there is any error; null when nothing on the page has a ruleset.
checked_types, unchecked_typesThe rulesets that were applied, and the declared types no ruleset covers.
errorsMissing required fields (WebSite: missing required name), a block that is not valid JSON, a block with no @context, or nesting deeper than 100 levels.
warningsMissing recommended fields (Organization: missing recommended contactPoint, description), one line per object, and a line when the page has no JSON-LD at all.
itemsUp to 50 objects, each with its type, the ruleset it was checked against, valid, missing_required, missing_recommended and its name.

Which types are checked

RulesetRequiredRecommended
Organizationname, urllogo, sameAs, contactPoint, description
LocalBusinessname, addresstelephone, openingHours (or openingHoursSpecification), geo, url, image, priceRange
WebSitename, urlpotentialAction
Productnameimage, description, brand, offers, aggregateRating, review
Offerprice, priceCurrencyavailability, url, priceValidUntil
Articleheadlineimage, datePublished, dateModified, author
NewsArticle, BlogPostingheadlineimage, datePublished, author
Eventname, startDate, locationendDate, offers, image, description
FAQPage / QuestionmainEntity / name, acceptedAnswer
BreadcrumbListitemListElement

Also checked: JobPosting, Recipe, Review, AggregateRating, SoftwareApplication, VideoObject, Person and PostalAddress. Twenty-two LocalBusiness subtypes (Restaurant, Dentist, Hotel, CafeOrCoffeeShop, Plumber, LegalService and others) are judged by the LocalBusiness rules, and 31 Organization subtypes (Corporation, NGO, OnlineStore, NewsMediaOrganization, EducationalOrganization and others) by the Organization rules.

A type with no ruleset, such as SearchAction, ImageObject or WebPage, comes back unchecked, with valid: null, never as invalid. Correct markup is not accused of being wrong because we hold no opinion on it.

What the generator writes

With generateMarkup set to missing (pages with no JSON-LD) or always (every page), the row also carries a suggestion built from the page: name from the page title, url, description from the meta or Open Graph description, a contactPoint with the first role email address on the page, and sameAs with its social profiles. The suggestion is run back through the validator:

FieldWhat goes in it
generated_jsonldThe suggested object.
generated_scriptThe same object inside a <script type="application/ld+json"> tag, with <, > and & escaped so a name containing </script> cannot break your page.
generated_validThe validator's verdict on the suggestion.
generated_missingRequired and recommended fields the suggestion still lacks, for you to fill in.

Who it is for

  • SEO agencies and in-house SEO auditing structured data across hundreds or thousands of pages at once, with errors and warnings as columns rather than one URL at a time in a browser.
  • Developers shipping structured data, as a check in a release routine: run the key templates after a deploy and look for valid: false.
  • Site migrations, where markup is often lost when templates change. Run the old and new URLs and compare types and errors row by row.
  • Small-business sites with no markup at all, where generateMarkup: missing gives a starting point to complete and publish.

How to use it

In the Apify Console:

  1. Paste full URLs, including https://, into Page URLs, up to 5,000. The input form rejects bare domains such as example.com.
  2. Leave Write suggested markup on "No, only validate", or choose "Only for pages with no JSON-LD" or "For every page". Pick the Type for suggested markup.
  3. Set a maximum cost per run if you want a ceiling. The actor stops before the row that would pass it.
  4. Start the run. The Pages view shows URL, status, whether the page has markup, its types, valid, errors and warnings. Export as CSV, Excel or JSON.

From the API, this call starts a run, waits for it and returns the rows:

curl -X POST \
"https://api.apify.com/v2/acts/locomint~schema-markup-validator/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"startUrls": [{"url": "https://www.python.org/"}], "generateMarkup": "missing"}'

The synchronous endpoint waits up to 300 seconds. For a long list, start the run with POST https://api.apify.com/v2/acts/locomint~schema-markup-validator/runs?token=YOUR_APIFY_TOKEN and read its dataset when the run finishes. With the Python client (

pip install apify-client
):

from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run = client.actor("locomint/schema-markup-validator").call(
run_input={"startUrls": [{"url": "https://coffeecollective.dk/"}], "generateMarkup": "always"}
)
for row in client.dataset(run.default_dataset_id).iterate_items():
print(row["url"], row["valid"], row["errors"], row.get("generated_missing"))

Input example

{
"startUrls": [
{ "url": "https://coffeecollective.dk/" },
{ "url": "https://www.monmouthcoffee.co.uk/" }
],
"generateMarkup": "always",
"markupType": "Organization",
"respectRobots": true,
"concurrency": 5,
"pageTimeout": 25
}
FieldDefaultAllowedWhat it does
startUrlsrequired1 to 5,000 URLsThe pages to check. Exact duplicates are dropped. Through the API, plain strings work as well as { "url": ... } objects.
generateMarkupoffoff, missing, alwaysWrite suggested JSON-LD never, only for pages with none, or for every page.
markupTypeOrganizationOrganization, LocalBusiness, Corporation, OnlineStore, ProfessionalService, Store, Restaurant, DentistThe @type the suggestion is written as.
respectRobotstruetrue / falseSkip a page that robots.txt disallows; the row arrives as robots_disallowed.
concurrency51 to 10Pages fetched at the same time.
pageTimeout255 to 60 secondsA page with no answer by then is delivered as unreachable.

Output example

A real row, from a run on 11 September 2026 with the input above. generated_script is left out here; it holds the same object as generated_jsonld inside a script tag.

{
"url": "https://coffeecollective.dk/",
"final_url": "https://coffeecollective.dk/",
"status": "ok",
"http_status": 200,
"has_markup": true,
"blocks": 2,
"types": ["Organization", "SearchAction", "WebSite"],
"valid": true,
"checked": true,
"checked_types": ["Organization", "WebSite"],
"unchecked_types": ["SearchAction"],
"errors": [],
"warnings": ["Organization: missing recommended contactPoint, description"],
"items": [
{"type": "Organization", "types": ["Organization"], "checked": true, "checked_against": "Organization", "valid": true, "missing_required": [], "missing_recommended": ["contactPoint", "description"], "name": "Coffee Collective"},
{"type": "WebSite", "types": ["WebSite"], "checked": true, "checked_against": "WebSite", "valid": true, "missing_required": [], "missing_recommended": [], "name": "Coffee Collective"},
{"type": "SearchAction", "types": ["SearchAction"], "checked": false, "checked_against": null, "valid": null, "missing_required": [], "missing_recommended": [], "name": null}
],
"note": null,
"checked_at": "2026-09-11T18:43:41+00:00",
"generated_jsonld": {
"@context": "https://schema.org",
"@type": "Organization",
"name": "Coffee Collective",
"url": "https://coffeecollective.dk/",
"description": "A specialty coffee roastery. We work directly with farmers to ensure quality, transparency, and sustainability - creating experiences from farmer to guest.",
"sameAs": [
"https://www.instagram.com/coffeecollectif",
"https://www.linkedin.com/company/coffeecollective"
]
},
"generated_valid": true,
"generated_missing": ["logo", "contactPoint"]
}

The suggestion adds the description the existing markup lacks. It has no contactPoint because no role address appears in the homepage HTML, so generated_missing asks for one. In an earlier run, python.org's WebSite object declared no name, so that page came back valid: false with the error WebSite: missing required name.

Status values

statusMeaning
okFetched and parsed. has_markup is false when the page carries no JSON-LD.
robots_disallowedrobots.txt disallows the path, so the page was not fetched; note quotes the rule.
refusedThe URL points at a private or internal network address.
blockedThe site answered with 403, 429 or a challenge page. Not retried.
unreachableNo connection, a DNS failure, or no answer within pageTimeout.
http_errorAny other 4xx or 5xx answer; http_status has the code.
not_htmlThe URL is a PDF, image or other non-text file.

Pricing

EventPrice
Page checked (one dataset row)$0.0005, which is $0.50 per 1,000
Actor start$0.00005 per GB of run memory, charged once per run

Worked example: auditing 2,000 pages of a site costs 2,000 x $0.0005 = $1.00, plus $0.00005 for starting a 1 GB run. 10,000 pages is two runs of 5,000 and $5.00. Apify's free plan gives $5 of usage a month without a card, which covers just under 10,000 pages.

You pay only these event prices; Apify compute is not billed to you separately, and generating markup costs nothing extra. Every URL produces one row and one charge, including blocked, unreachable and robots_disallowed, because each is an answer about that URL. Entries that are not URLs are skipped, named in the run log and not charged. If you set a maximum cost per run, the actor stops before the row that would pass it.

FAQ

Does it read microdata or RDFa?

No, JSON-LD only. A page whose structured data is written as microdata or RDFa comes back with has_markup: false and a warning that says those formats are not read.

Why is a type unchecked instead of valid or invalid?

We hold rules for 20 types and their listed subtypes. Anything else, such as SearchAction or ImageObject, is reported under unchecked_types with valid: null, because calling correct markup invalid is worse than saying nothing.

What is the difference between an error and a warning?

An error is a missing required field or a block that cannot be read as schema.org, and it makes valid false. A warning is a missing recommended field; the object still passes. These rules say what a complete object needs; passing them does not guarantee that a search engine shows a rich result.

Why is valid null on a page that has markup?

Because every object on it was of a type we do not check, so nothing was judged. null means "no opinion", never a clean pass.

Should I publish the generated markup as it is?

Review it first. name is the page title as written, so a title such as "Home - Monmouth Coffee Company" (from our own test run) needs editing, and the generator never writes an address, phone number, opening hours or logo. With a LocalBusiness-family type the suggestion is not valid until you add address, which is required.

Does it see markup added by JavaScript?

No. It reads the HTML the server sends. JSON-LD that a tag manager or a JavaScript framework inserts after the page loads is not in that HTML and is not seen.

Does it honour robots.txt, and what happens when a site blocks it?

It reads each site's robots.txt once per run and applies the rules for all crawlers (the * group); a disallowed page comes back robots_disallowed. A 403, 429 or challenge page is reported as blocked and never retried from the same address or any other, and the host is left alone for 15 minutes.

Limits

  • JSON-LD only; no microdata or RDFa. No JavaScript rendering.
  • One page per URL; the actor does not crawl. 5,000 URLs per run, 25 seconds per page by default (5 to 60), 3 MB of HTML per page, at most 5 redirects.
  • Rules check that a field is present, not what it holds: a telephone of "n/a" counts as present, and dates, URLs and prices are not validated.
  • Subtypes outside the listed ones, such as ClothingStore or AccountingService, come back unchecked.
  • items lists the first 50 objects on a page; types, errors and warnings cover all of them. Warnings repeat per object, so four Organization objects give four lines.
  • The generator reads only the page itself: role email addresses on other pages are not used, and mailboxes that name a person are never written into a suggestion.

Other Locomint actors