Compiling Custom Ajv Rulesets to Enforce Google Rich Result Eligibility Criteria

Bridge the Gap Between Schema.org Definitions and Google Eligibility

Schema.org is an open, permissive vocabulary. Its specifications treat almost every property as optional and allow broad type ambiguity to maximize web adoption. Conversely, Google’s Rich Result algorithms are strict, deterministic, and restrictive. A payload that passes basic Schema.org syntax checks can easily fail to generate a SERP enhancement because it lacks a required nested node, uses an unindexed ISO currency format, or fails conditional requirements—such as requiring an offers, review, or aggregateRating entity within a Product payload.

Relying on post-deployment tools like Google’s Rich Results Test API exposes production sites to silent regressions, rate-limiting, and slow validation cycles. Engineering-led technical teams address this by integrating deterministic schema validators into their deployment pipelines. As explored in Automated JSON-LD Schema Validation Gates Using Custom Headless Test Suites, testing JSON-LD at the build stage prevents structural defects from reaching search engine crawlers. Implementing Ajv (Another JSON Schema Validator) allows teams building modern SEO automation tools to construct local, high-throughput validation suites that precisely replicate Google Search eligibility criteria.

Engineering Rich-Result-Compliant JSON Schemas

Google’s documentation defines strict requirements for structured data features. To capture these requirements, schemas must use JSON Schema Draft 7 or Draft 2020-12 semantics to enforce required attributes, specific types, and complex conditional requirements.

Consider the criteria for a Google Product rich snippet. Google requires either offers, review, or aggregateRating. Furthermore, if offers is provided, fields like price, priceCurrency, and availability become mandatory. The schema definition below enforces these operational constraints:

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://internal.engine.local/schemas/google-product.json",
  "type": "object",
  "required": ["@context", "@type", "name", "image"],
  "properties": {
    "@context": {
      "type": "string",
      "pattern": "^https?://schema\.org/?$"
    },
    "@type": {
      "type": "string",
      "const": "Product"
    },
    "name": {
      "type": "string",
      "minLength": 1
    },
    "image": {
      "oneOf": [
        { "type": "string", "format": "uri" },
        {
          "type": "array",
          "items": { "type": "string", "format": "uri" },
          "minItems": 1
        }
      ]
    },
    "offers": { "$ref": "#/$defs/offerDefinition" },
    "aggregateRating": { "$ref": "#/$defs/aggregateRatingDefinition" },
    "review": { "$ref": "#/$defs/reviewDefinition" }
  },
  "anyOf": [
    { "required": ["offers"] },
    { "required": ["aggregateRating"] },
    { "required": ["review"] }
  ],
  "$defs": {
    "offerDefinition": {
      "type": "object",
      "required": ["@type", "price", "priceCurrency"],
      "properties": {
        "@type": { "type": "string", "const": "Offer" },
        "price": {
          "oneOf": [
            { "type": "number", "minimum": 0 },
            { "type": "string", "pattern": "^[0-9]+(\.[0-9]{2})?$" }
          ]
        },
        "priceCurrency": {
          "type": "string",
          "format": "iso-4217"
        },
        "availability": {
          "type": "string",
          "pattern": "^https?://schema\.org/(InStock|OutOfStock|PreOrder|BackOrder)$"
        }
      }
    },
    "aggregateRatingDefinition": {
      "type": "object",
      "required": ["@type", "ratingValue", "reviewCount"],
      "properties": {
        "@type": { "type": "string", "const": "AggregateRating" },
        "ratingValue": { "type": "number", "minimum": 1, "maximum": 5 },
        "reviewCount": { "type": "integer", "minimum": 1 }
      }
    },
    "reviewDefinition": {
      "type": "object",
      "required": ["@type", "author", "reviewRating"],
      "properties": {
        "@type": { "type": "string", "const": "Review" },
        "author": {
          "type": "object",
          "required": ["@type", "name"],
          "properties": {
            "@type": { "type": "string", "enum": ["Person", "Organization"] },
            "name": { "type": "string" }
          }
        },
        "reviewRating": {
          "type": "object",
          "required": ["@type", "ratingValue"],
          "properties": {
            "@type": { "type": "string", "const": "Rating" },
            "ratingValue": { "type": "number", "minimum": 1, "maximum": 5 }
          }
        }
      }
    }
  }
}

Extending Ajv with Custom Keywords and Formats

Standard JSON Schema assertions cannot validate domain-specific SEO rules out of the box, such as verifying valid ISO 4217 currency designations, ISO 8601 durations (required for Recipe cook times and VideoObject durations), or cross-property dependencies. Ajv v8 allows you to introduce custom formats and keyword compilers to enforce these requirements with zero external runtime dependencies.

Enforcing ISO Formats

Google demands strict adherence to ISO 4217 (three-letter currency codes) and ISO 8601 (dates and durations). The following script registers custom formats within an Ajv instance:

import Ajv from "ajv/dist/2020.js";
import addFormats from "ajv-formats";

const ajv = new Ajv({
  allErrors: true,
  strict: true,
  coerceTypes: false
});

addFormats(ajv);

// Add ISO 4217 three-letter currency code validation
const ISO_4217_CODES = new Set(["USD", "EUR", "GBP", "CAD", "AUD", "JPY", "INR"]);

ajv.addFormat("iso-4217", {
  type: "string",
  validate: (currency) => ISO_4217_CODES.has(currency.toUpperCase())
});

// Add ISO 8601 duration validation (e.g., PT1H30M)
const ISO_8601_DURATION_REGEX = /^P(?!$)((d+D)|(d+M(d+D)?)|(d+Y(d+M)?(d+D)?))?(T(?=d)(d+H)?(d+M)?(d+S)?)?$/;

ajv.addFormat("iso-8601-duration", {
  type: "string",
  validate: (duration) => ISO_8601_DURATION_REGEX.test(duration)
});

Custom Keywords for Complex Nesting

Enterprise publishing systems often emit structured data using @graph arrays containing interdependent entities, such as a BreadcrumbList, WebPage, and Article linked via @id strings. Standard JSON Schema definitions struggle to validate that an entity referenced by an @id exists elsewhere in the graph array.

You can compile a custom keyword, idReferenceExists, to validate target references directly within Ajv:

ajv.addKeyword({
  keyword: "idReferenceExists",
  type: "object",
  schemaType: "string",
  compile: (targetType) => {
    return (data, dataValidationContext) => {
      // Ascend to the root @graph array
      const rootGraph = dataValidationContext?.rootData?.["@graph"];
      if (!Array.isArray(rootGraph)) {
        return false;
      }

      const targetId = data["@id"];
      if (!targetId) return false;

      // Verify at least one entity matches the target ID and expected @type
      return rootGraph.some(
        (entity) => entity["@id"] === targetId && entity["@type"] === targetType
      );
    };
  }
});

AOT Compilation for High-Performance CI/CD Validation

In high-scale enterprise environments running continuous integration checks across tens of thousands of dynamic pages, compiling JSON Schemas just-in-time (JIT) inside test workers introduces noticeable CPU overhead and increases memory consumption. Compiling Ajv schemas ahead-of-time (AOT) to standalone JavaScript code eliminates schema compilation overhead during test execution.

Building the Compilation Script

Ajv provides standalone code generation through the ajv/dist/standalone module. The script below reads schema configurations, binds all custom keywords and formats, and generates an optimized, standalone JavaScript validator function:

// compile-validators.js
import fs from "node:fs";
import path from "node:path";
import Ajv from "ajv/dist/2020.js";
import standaloneCode from "ajv/dist/standalone/index.js";
import addFormats from "ajv-formats";

const ajv = new Ajv({
  code: { source: true, esm: true },
  allErrors: true
});

addFormats(ajv);

// Register application formats
ajv.addFormat("iso-4217", {
  type: "string",
  validate: (x) => /^[A-Z]{3}$/.test(x)
});

// Load schemas
const productSchema = JSON.parse(
  fs.readFileSync(path.resolve("./schemas/google-product.json"), "utf-8")
);

// Map mapping exports
const validateProduct = ajv.compile(productSchema);

const moduleCode = standaloneCode(ajv, {
  validateProduct: validateProduct
});

fs.writeFileSync(path.resolve("./dist/validate-rulesets.js"), moduleCode);
console.log("Standalone validation module compiled successfully.");

Run the script during your application’s build phase:

node compile-validators.js

The resulting validate-rulesets.js module is pure JavaScript with no dynamic eval, minimal memory requirements, and fast execution times, making it ideal for edge execution and local testing suites.

Resolving Graphs and Array-Wrapped Structures

Production payloads rarely wrap a single JSON-LD entity in isolation. Instead, modern content systems export an entity graph containing an array of nodes:

{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "Organization",
      "@id": "https://example.com/#organization",
      "name": "Acme Corp"
    },
    {
      "@type": "Product",
      "@id": "https://example.com/#product",
      "name": "Super Widget",
      "image": "https://example.com/widget.jpg",
      "offers": {
        "@type": "Offer",
        "price": 29.99,
        "priceCurrency": "USD"
      }
    }
  ]
}

To evaluate these multi-node graphs with pre-compiled validators, the test harness should isolate the primary entity before running validation checks:

import { validateProduct } from "./dist/validate-rulesets.js";

export function evaluateJsonLdGraph(rawJsonLd) {
  let targetData = rawJsonLd;

  if (Array.isArray(rawJsonLd["@graph"])) {
    const matched = rawJsonLd["@graph"].find(
      (node) => node["@type"] === "Product"
    );
    if (!matched) {
      return {
        valid: false,
        errors: [{ message: "Missing required Product node inside @graph payload" }]
      };
    }
    // Retain a reference to the root document to preserve cross-node resolution
    targetData = matched;
  }

  const isValid = validateProduct(targetData);

  return {
    valid: isValid,
    errors: validateProduct.errors || []
  };
}

Integrating Compiled Validators into Automation Suites

Compiling validation logic into lightweight, standalone units allows you to run deterministic checks directly inside modern SEO automation tools and CI/CD pipelines. This ensures schema eligibility is verified locally on pull requests long before code is deployed to staging or production.

The example below demonstrates how to integrate the compiled validator with Playwright to verify dynamic pages without calling external APIs:

import { test, expect } from "@playwright/test";
import { evaluateJsonLdGraph } from "./evaluateJsonLdGraph.js";

test.describe("Structured Data CI Quality Gate", () => {
  test("E-commerce PDP must emit valid Google-eligible Product schema", async ({ page }) => {
    await page.goto("/products/super-widget", { waitUntil: "networkidle" });

    const scripts = await page.$$eval(
      'script[type="application/ld+json"]',
      (elements) => elements.map((el) => el.textContent)
    );

    expect(scripts.length).toBeGreaterThan(0);

    let parsedProductPayload = null;

    for (const jsonText of scripts) {
      if (!jsonText) continue;
      try {
        const parsed = JSON.parse(jsonText);
        if (
          parsed["@type"] === "Product" ||
          (Array.isArray(parsed["@graph"]) &&
            parsed["@graph"].some((n) => n["@type"] === "Product"))
        ) {
          parsedProductPayload = parsed;
          break;
        }
      } catch (err) {
        throw new Error(`JSON syntax error in payload: ${err.message}`);
      }
    }

    expect(parsedProductPayload).not.toBeNull();

    const result = evaluateJsonLdGraph(parsedProductPayload);

    if (!result.valid) {
      console.error("Ajv Google Eligibility Validation Errors:", JSON.stringify(result.errors, null, 2));
    }

    expect(result.valid).toBe(true);
  });
});

This automated validation approach provides several key technical advantages over traditional testing methods:

  • Full Execution Independence: Validation runs locally in memory without making external network calls to third-party APIs.
  • Consistent Validation Criteria: Eliminates intermittent failures caused by external API rate limits or network issues during CI/CD runs.
  • Precise, Actionable Feedback: Ajv pinpoints errors down to the exact JSON path (e.g., /offers/priceCurrency), helping developers identify and fix missing properties immediately.
  • Guaranteed Search Feature Parity: By capturing Google’s structured data rules in your own schema definitions, you prevent rich result regressions on every build.

Related Guides in This Series

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *