Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions gallery/Product/invalid_type.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"@context": "https://schema.org",
"@type": "BananaPhone",
"name": "Invalid Type Test"
}
22 changes: 22 additions & 0 deletions src/types/__tests__/schemaOrg.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ describe('Schema.org Validator', () => {
Brand: [MockValidator],
Organization: [MockValidator],
Offer: [MockValidator],
VideoObject: [MockValidator],
SeekToAction: [MockValidator],
Clip: [MockValidator],
BroadcastEvent: [MockValidator],
// Invalid type for testing - no type-specific handler, only global schemaOrg handler runs
BananaPhone: [MockValidator],
};
});

Expand Down Expand Up @@ -156,6 +162,22 @@ describe('Schema.org Validator', () => {
errorType: 'schemaOrg',
});
});

it('should return an error if an invalid type was detected', async () => {
const data = await loadTestData('Product/invalid_type.json', 'jsonld');

const issues = await validator.validate(data);

expect(issues).to.have.lengthOf(1);
expect(issues[0]).to.deep.include({
rootType: 'BananaPhone',
issueMessage: 'Type "BananaPhone" is not a valid schema.org type',
severity: 'ERROR',
path: [{ type: 'BananaPhone', index: 0 }],
errorType: 'schemaOrg',
fieldName: '@type',
});
});
});

describe('Microdata', () => {
Expand Down
34 changes: 31 additions & 3 deletions src/types/schemaOrg.js
Original file line number Diff line number Diff line change
Expand Up @@ -178,17 +178,29 @@ export default class SchemaOrgValidator {
return false;
}

// Strip -input or -output suffix if present (schema.org Actions extension)
// See: https://schema.org/docs/actions.html#part-4
let propertyToCheck = property;
if (property.endsWith('-input') || property.endsWith('-output')) {
propertyToCheck = property.replace(/-(input|output)$/, '');
}

// Check if property is directly supported
if (schema[type].properties.includes(property)) {
if (schema[type].properties.includes(propertyToCheck)) {
return true;
}

// Check if property is supported through inheritance
return Object.keys(schema[type].propertiesFromParent).some((parent) => {
return schema[type].propertiesFromParent[parent].includes(property);
return schema[type].propertiesFromParent[parent].includes(propertyToCheck);
});
}

async validateType(type) {
const schema = await this.#loadSchema();
return !!schema[type];
}

async validate(data) {
const issues = [];

Expand All @@ -197,6 +209,22 @@ export default class SchemaOrgValidator {
return [];
}

const typeId = this.#stripSchema(this.type);

// Check if type exists in schema.org
const typeExists = await this.validateType(typeId);
if (!typeExists) {
issues.push({
issueMessage: `Type "${typeId}" is not a valid schema.org type`,
severity: 'ERROR',
path: this.path,
errorType: 'schemaOrg',
fieldName: '@type',
});
// Skip property validation since type is invalid
return issues;
}

// Get list of properties, any other keys which do not start with @
const properties = Object.keys(data).filter(
(key) => !key.startsWith('@'),
Expand All @@ -206,7 +234,6 @@ export default class SchemaOrgValidator {
await Promise.all(
properties.map(async (property) => {
const propertyId = this.#stripSchema(property);
const typeId = this.#stripSchema(this.type);

const isValid = await this.validateProperty(typeId, propertyId);
if (!isValid) {
Expand All @@ -215,6 +242,7 @@ export default class SchemaOrgValidator {
severity: 'WARNING',
path: this.path,
errorType: 'schemaOrg',
fieldName: propertyId,
});
}
}),
Expand Down
7 changes: 5 additions & 2 deletions src/validator.js
Original file line number Diff line number Diff line change
Expand Up @@ -105,14 +105,17 @@ export class Validator {

// Find supported handlers
const handlers = [...(this.registeredHandlers[type] || [])];
if (!handlers || handlers.length === 0) {
if (handlers.length === 0) {
this.debug &&
console.warn(
`${spacing} WARN: No handlers registered for type: ${type}`,
);
return [];
}
// Always run global handlers (e.g., schemaOrg) even if no type-specific handler exists
handlers.push(...(this.globalHandlers || []));
if (handlers.length === 0) {
return [];
}

const handlerPromises = handlers.map(async (handler) => {
const handlerClass = (await handler()).default;
Expand Down