Skip to content

feat: Add support for optional custom error messages by using ajv-errors #44

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
53 changes: 53 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,57 @@ jobs:
files: .github/workflows/**.yml
```

### Example with Custom Error Messages

When `custom-errors` is enabled, you can use the `errorMessage` keyword in your
JSON schema to provide more user-friendly error messages. This is powered by the
[ajv-errors](https://github.com/ajv-validator/ajv-errors) library.

```yaml
jobs:
validate-config:
name: Validate configuration files
runs-on: ubuntu-latest
steps:
- name: Validate config with custom errors
uses: dsanders11/json-schema-validate-action@v1.3.0
with:
schema: ./config.schema.json
files: ./config/*.yml
custom-errors: true
```

Example schema with custom error messages:

```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"type": "object",
"properties": {
"name": {
"type": "string",
"minLength": 1
},
"version": {
"type": "string",
"pattern": "^\\d+\\.\\d+\\.\\d+$"
}
},
"required": ["name", "version"],
"errorMessage": {
"type": "Configuration must be an object",
"required": {
"name": "Configuration must have a 'name' property",
"version": "Configuration must have a 'version' property"
},
"properties": {
"name": "Name must be a non-empty string",
"version": "Version must follow semantic versioning (e.g., '1.0.0')"
}
}
}
```

### Validating Schema

Schemas can be validated by setting the `schema` input to the string literal
Expand All @@ -56,6 +107,8 @@ simply set a URL fragment (e.g. `#bust-cache`) on the schema URL.
`true`)
- `all-errors` - Whether to report all errors or stop after the first (default:
`false`)
- `custom-errors` - Enable support for custom error messages using ajv-errors
(default: `false`)

### Outputs

Expand Down
102 changes: 102 additions & 0 deletions __tests__/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -595,4 +595,106 @@ describe('action', () => {
);
});
});

describe('custom error messages', () => {
it('forces allErrors to true when custom-errors is enabled', async () => {
mockGetBooleanInput({ 'custom-errors': true, 'all-errors': false });
mockGetInput({ schema });
mockGetMultilineInput({ files });

vi.mocked(fs.readFile)
.mockResolvedValueOnce(schemaContents)
.mockResolvedValueOnce('invalid content');
mockGlobGenerator(['/foo/bar/baz/config.yml']);

await main.run();
expect(runSpy).toHaveReturned();
expect(process.exitCode).not.toBeDefined();

// Should report multiple errors even though all-errors was false
expect(core.error).toHaveBeenCalledTimes(4);
expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenLastCalledWith('valid', false);
});

it('provides custom error messages when validation fails', async () => {
mockGetBooleanInput({ 'custom-errors': true, 'fail-on-invalid': true });
mockGetInput({ schema });
mockGetMultilineInput({ files });

// Create a schema with custom error messages
const customErrorSchemaContents = JSON.stringify({
title: 'Test schema with custom errors',
$schema: 'http://json-schema.org/draft-07/schema#',
type: 'object',
properties: {
name: { type: 'string', minLength: 1 }
},
required: ['name'],
errorMessage: {
properties: {
name: 'Name must be a non-empty string'
}
}
});

const invalidInstanceContents = JSON.stringify({ name: '' });

vi.mocked(fs.readFile)
.mockResolvedValueOnce(customErrorSchemaContents)
.mockResolvedValueOnce(invalidInstanceContents);
mockGlobGenerator(['/foo/bar/baz/config.yml']);

await main.run();
expect(runSpy).toHaveReturned();
expect(process.exitCode).toEqual(1);

expect(core.error).toHaveBeenCalledWith(
'Error while validating file: /foo/bar/baz/config.yml'
);

// Check that we get our custom error message in the JSON output
const errorCalls = vi.mocked(core.error).mock.calls;
const hasCustomMessage = errorCalls.some(
call =>
typeof call[0] === 'string' &&
call[0].includes('Name must be a non-empty string')
);
expect(hasCustomMessage).toBe(true);

expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenLastCalledWith('valid', false);
});

it('works without custom-errors when disabled', async () => {
mockGetBooleanInput({ 'custom-errors': false, 'fail-on-invalid': false });
mockGetInput({ schema });
mockGetMultilineInput({ files });

vi.mocked(fs.readFile)
.mockResolvedValueOnce(schemaContents)
.mockResolvedValueOnce('invalid content');
mockGlobGenerator(['/foo/bar/baz/config.yml']);

await main.run();
expect(runSpy).toHaveReturned();
expect(process.exitCode).not.toBeDefined();

expect(core.error).toHaveBeenCalledWith(
'Error while validating file: /foo/bar/baz/config.yml'
);

// Should NOT have any custom error messages (no errorMessage keyword)
const errorCalls = vi.mocked(core.error).mock.calls;
const hasCustomErrors = errorCalls.some(
call =>
typeof call[0] === 'string' &&
call[0].includes('"keyword":"errorMessage"')
);
expect(hasCustomErrors).toBe(false);

expect(core.setOutput).toHaveBeenCalledTimes(1);
expect(core.setOutput).toHaveBeenLastCalledWith('valid', false);
});
});
});
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ inputs:
description: Report all errors instead of stopping at the first
required: false
default: false
custom-errors:
description: Enable support for custom error messages using ajv-errors
required: false
default: false

outputs:
valid:
Expand Down
Loading