Employer Aggregate Rating Schema Framework
Use conditional-logic schema generation to keep Employer Aggregate Rating markup aligned with real page state and avoid stale or invalid static templates.
When To Use It
Google Search CentralEmployer Aggregate Rating documentationUse on a page where users can rate an employer on your site and the aggregate rating is visible on that page.
Key Implementation Documentation Highlights
- Required properties: itemReviewed, ratingValue, ratingCount or reviewCount. Google lists both counts as required, but one of them is enough. itemReviewed is an Organization that requires name; sameAs is recommended.
- Recommended properties: bestRating (defaults to 5) and worstRating (defaults to 1).
- ratingValue is a number, fraction or percentage and must sit within worstRating to bestRating (1 to 5 by default).
- Ratings must be available to users from the page with the markup; users must be able to post their own ratings on your site, and your site must host those ratings.
- Google expects content parity: every marked-up value must match what users can see on the page.
- Validate with the Rich Results Test and URL Inspection, then monitor the related Search Console report for errors and warnings after deployment.
Template Approach
const pageData = {
employerName: "Example Retail Group",
employerUrl: "https://www.example.org",
averageRating: 4.2,
ratingCount: 318,
reviewCount: 127
};
const staticTemplate = {
"@context": "https://schema.org",
"@type": "EmployerAggregateRating",
"itemReviewed": {
"@type": "Organization",
"name": pageData.employerName,
"sameAs": pageData.employerUrl
},
"ratingValue": pageData.averageRating,
"ratingCount": pageData.ratingCount,
"reviewCount": pageData.reviewCount,
"bestRating": 5,
"worstRating": 1
};
{
"@context": "https://schema.org",
"@type": "EmployerAggregateRating",
"itemReviewed": {
"@type": "Organization",
"name": "Example Retail Group",
"sameAs": "https://www.example.org"
},
"ratingValue": 4.2,
"ratingCount": 318,
"reviewCount": 127,
"bestRating": 5,
"worstRating": 1
}
Conditional-logic Framework
const ISO_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2}))?(Z|[+-]\d{2}:\d{2})?)?$/;
function isAbsoluteUrl(value) {
return typeof value === "string" && /^https?:\/\/[^\s/?#]+\.[^\s/?#]+(?:[/?#]\S*)?$/i.test(value);
}
// Returns the ISO 8601 string unchanged (timezone offset kept), or null if unparseable.
function toIsoDateTime(value) {
if (typeof value !== "string") return null;
const text = value.trim();
const m = text.match(ISO_DATE_TIME);
if (!m) return null;
const [, y, mo, d, h = "00", mi = "00", s = "00"] = m;
const date = new Date(Date.UTC(+y, +mo - 1, +d));
if (date.getUTCMonth() !== +mo - 1 || date.getUTCDate() !== +d) return null;
if (+h > 23 || +mi > 59 || +s > 59) return null;
return text;
}
function toIsoDate(value) {
const iso = toIsoDateTime(value);
return iso ? iso.slice(0, 10) : null;
}
// Drops null, undefined, "", [] and {} recursively.
function compact(value) {
if (Array.isArray(value)) {
const items = value.map(compact).filter((v) => v !== undefined);
return items.length ? items : undefined;
}
if (value && typeof value === "object") {
const out = {};
for (const [key, v] of Object.entries(value)) {
const c = compact(v);
if (c !== undefined) out[key] = c;
}
return Object.keys(out).length ? out : undefined;
}
return value === null || value === undefined || value === "" ? undefined : value;
}
function passesPageGates(source) {
if (source.indexable === false) return false;
if (source.canonicalUrl && source.url && source.canonicalUrl !== source.url) return false;
return source.contentVisible !== false;
}
function toNumber(value) {
if (value === null || value === undefined || value === "" || typeof value === "boolean") return null;
const number = Number(value);
return Number.isFinite(number) ? number : null;
}
function toCount(value) {
return Number.isInteger(value) && value >= 0 ? value : null;
}
function buildEmployerRatingSchema(source) {
// (a) Page gates: indexable, self-canonical, ratings visible on this page.
if (!passesPageGates(source)) return null;
// Users must be able to post ratings on this site, and the site must host them.
if (!source.acceptsUserRatings || !source.ratingsHostedOnSite) return null;
// (b) Required: itemReviewed (Organization with name), ratingValue, ratingCount or reviewCount.
const employer = source.employer || {};
const ratingCount = toCount(source.ratingCount);
const reviewCount = toCount(source.reviewCount);
if (!employer.name || (ratingCount === null && reviewCount === null)) return null;
// (d) ratingValue must sit within worstRating..bestRating (defaults 1..5).
const bestRating = toNumber(source.bestRating) ?? 5;
const worstRating = toNumber(source.worstRating) ?? 1;
const ratingValue = toNumber(source.ratingValue);
if (ratingValue === null) return null;
if (worstRating >= bestRating || ratingValue < worstRating || ratingValue > bestRating) return null;
// (c) Required + recommended properties, only when the source has them.
const schema = {
"@context": "https://schema.org",
"@type": "EmployerAggregateRating",
itemReviewed: {
"@type": "Organization",
name: employer.name,
sameAs: isAbsoluteUrl(employer.url) ? employer.url : null
},
ratingValue,
ratingCount,
reviewCount,
bestRating,
worstRating
};
// (e) Strip empty values.
return compact(schema);
}
// Usage:
// const source = {
// "url": "https://www.example.com/companies/example-retail-group/ratings",
// "canonicalUrl": "https://www.example.com/companies/example-retail-group/ratings",
// "indexable": true,
// "contentVisible": true,
// "acceptsUserRatings": true,
// "ratingsHostedOnSite": true,
// "employer": {
// "name": "Example Retail Group",
// "url": "https://www.example.org"
// },
// "ratingValue": 4.2,
// "ratingCount": 318,
// "reviewCount": 127,
// "bestRating": 5,
// "worstRating": 1
// };
// const schema = buildEmployerRatingSchema(source); // null when a gate or required property fails
import re
from datetime import date, datetime, timezone
ISO_DATE_TIME = re.compile(r"^(\d{4})-(\d{2})-(\d{2})(?:T(\d{2}):(\d{2})(?::(\d{2}))?(Z|[+-]\d{2}:\d{2})?)?$")
ABSOLUTE_URL = re.compile(r"^https?://[^\s/?#]+\.[^\s/?#]+(?:[/?#]\S*)?$", re.I)
def is_absolute_url(value):
return isinstance(value, str) and bool(ABSOLUTE_URL.match(value))
# Returns the ISO 8601 string unchanged (timezone offset kept), or None if unparseable.
def to_iso_date_time(value):
if not isinstance(value, str):
return None
text = value.strip()
m = ISO_DATE_TIME.match(text)
if not m:
return None
y, mo, d = int(m[1]), int(m[2]), int(m[3])
try:
date(y, mo, d)
except ValueError:
return None
if int(m[4] or 0) > 23 or int(m[5] or 0) > 59 or int(m[6] or 0) > 59:
return None
return text
def to_iso_date(value):
iso = to_iso_date_time(value)
return iso[:10] if iso else None
# Drops None, "", [] and {} recursively.
def compact(value):
if isinstance(value, list):
items = [c for c in (compact(v) for v in value) if c is not None]
return items or None
if isinstance(value, dict):
out = {k: c for k, c in ((k, compact(v)) for k, v in value.items()) if c is not None}
return out or None
return None if value is None or value == "" else value
def passes_page_gates(source):
if source.get("indexable") is False:
return False
if source.get("canonicalUrl") and source.get("url") and source["canonicalUrl"] != source["url"]:
return False
return source.get("contentVisible") is not False
def to_count(value):
return value if isinstance(value, int) and not isinstance(value, bool) and value >= 0 else None
def to_number(value):
if isinstance(value, bool) or value is None:
return None
try:
number = float(value)
except (TypeError, ValueError):
return None
if number != number or number in (float("inf"), float("-inf")):
return None
return int(number) if number.is_integer() else number
def build_employer_rating_schema(source):
# (a) Page gates: indexable, self-canonical, ratings visible on this page.
if not passes_page_gates(source):
return None
# Users must be able to post ratings on this site, and the site must host them.
if not source.get("acceptsUserRatings") or not source.get("ratingsHostedOnSite"):
return None
# (b) Required: itemReviewed (Organization with name), ratingValue, ratingCount or reviewCount.
employer = source.get("employer") or {}
rating_count = to_count(source.get("ratingCount"))
review_count = to_count(source.get("reviewCount"))
if not employer.get("name") or (rating_count is None and review_count is None):
return None
# (d) ratingValue must sit within worstRating..bestRating (defaults 1..5).
best = to_number(source.get("bestRating"))
worst = to_number(source.get("worstRating"))
best = 5 if best is None else best
worst = 1 if worst is None else worst
rating_value = to_number(source.get("ratingValue"))
if rating_value is None:
return None
if worst >= best or rating_value < worst or rating_value > best:
return None
# (c) Required + recommended properties, only when the source has them.
schema = {
"@context": "https://schema.org",
"@type": "EmployerAggregateRating",
"itemReviewed": {
"@type": "Organization",
"name": employer["name"],
"sameAs": employer.get("url") if is_absolute_url(employer.get("url")) else None,
},
"ratingValue": rating_value,
"ratingCount": rating_count,
"reviewCount": review_count,
"bestRating": best,
"worstRating": worst,
}
# (e) Strip empty values.
return compact(schema)
{
"@context": "https://schema.org",
"@type": "EmployerAggregateRating",
"itemReviewed": {
"@type": "Organization",
"name": "Example Retail Group",
"sameAs": "https://www.example.org"
},
"ratingValue": 4.2,
"ratingCount": 318,
"reviewCount": 127,
"bestRating": 5,
"worstRating": 1
}
The Output As An Entity Graph
The conditional-logic output above, parsed in your browser and drawn as the graph a crawler builds from it. Every nested object is a node and the property that holds it is the edge. Tap a node to inspect its properties, or drag to rearrange.
Why Conditional Logic Is Better Than Static Templates
- Prevents emitting empty, null, or stale fields that frequently trigger rich result warnings.
- Supports multiple page states (for example: no rating yet, no image yet, no offers yet) without duplicate templates.
- Lets engineering teams centralize schema policy checks and validation in reusable code paths.
- Makes large-scale schema maintenance safer when content models evolve over time.
Property Reference
What Google's Employer aggregate rating documentation requires and recommends, property by property. These are the same rules the SchemaCDN validator checks. Each name links to its schema.org definition.
Required, one of: At least one of ratingCount or reviewCount is required.
| Property | Expects | Notes | |
|---|---|---|---|
| itemReviewed | Required | Organization | The organization that is being rated. |
| itemReviewed.name | Required | Text | The name of the organization being rated. |
| itemReviewed.sameAs | Recommended | URL | The URL of the organization being rated. |
| ratingCount | Required | Number | The total number of ratings of the organization on your site. |
| ratingValue | Required | Number or Text | A numerical quality rating for the organization. |
| reviewCount | Required | Number | The number of people who provided a review with or without a rating. |
| bestRating | Recommended | Number | The highest value allowed in this rating system. |
| worstRating | Recommended | Number | The lowest value allowed in this rating system. |