Repository: stoplightio/spectral
Pull request #215 (2019-05-17 17:25:09)

feat: improve our AJV usage
### Please check if the PR fulfills these requirements
- [x] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)

### What kind of change does this PR introduce?

Feature (fewer messages?) + fixes for https://github.com/stoplightio/spectral/issues/187 https://github.com/stoplightio/spectral/issues/116

### What is the current behavior?
![image](https://user-images.githubusercontent.com/9273484/57945089-37263d00-78d9-11e9-9ecb-cfc60015caa0.png)


### If this is a feature change, what is the new behavior?
![image](https://user-images.githubusercontent.com/9273484/57945061-21187c80-78d9-11e9-91b7-785a5bd3e957.png)

### Does this PR introduce a breaking change?

Not really :)

### Other information

My initial intention was to make CLI messages more flexible, but this turned out to be not the greatest idea as we actually need meaningful messages only for stylish CLI format. API usage and JSON CLI format should be untouched, so decided to add the simple condition, so that both worlds are satisfied.

(e.g. detailed explanation, related issues, links for us to have context, eg. stackoverflow, issues outside of the repo, forum, etc.)


marbemac: Another thought, might be worth evaluating https://github.com/atlassian/better-ajv-errors.
P0lip: > I would like to see our summary generally be more useful, it should be a very quick hint as to what is going on, with a little information about the field or whatever. Those in the know will look at this short summary and know how to fix it, but others will want more information.

I'd drop summary entirely. Not really sure what's the point of that property.
Ideally, we should have something similar to what ESLint does. That is:
```

I'm not sure whether it makes sense to include property given the fact path is already present.
There is path available
P0lip: > I would like to see our summary generally be more useful, it should be a very quick hint as to what is going on, with a little information about the field or whatever. Those in the know will look at this short summary and know how to fix it, but others will want more information.

I'd drop summary entirely. Not really sure what's the point of that property.
Ideally, we should have something similar to the snippet below (inspired by ESLint)
```
{
    rule: "oas2-schema",
    message: "Expected something to be truthy",
    path: ['some','path']
    severity: 2,
    line: 100,
    column: 2
}
```
I'm not sure whether it makes sense to include property given the fact path is already present.
It'll also make things more difficult in Studio as we already do highlight the line and point to given line and column etc, so we would most likely want to avoid any long messages.
I'm open to discussion though. I do agree that the current solution is not ideal.

> This more information might have strings we've written or other variables involved, with maybe information on how to fix it, etc.

Agreed, would be nice to give some feedback. I'd say the best way to do it is to provide an extensive doc about what given rule does and give examples when it is met and when not.
Yet again, ESLint is a good example here
https://eslint.org/docs/rules/
https://eslint.org/docs/rules/eqeqeq
https://eslint.org/docs/rules/no-unexpected-multiline
philsturgeon: Literally putting everything through the os validator, vrs adding a few extra rules from that validator, is conceptually different enough that it doesn’t feel quite as unsettling.  

--
Phil Sturgeon
@philsturgeon

> On May 19, 2019, at 19:00, Vincenzo Chianese <notifications@github.com> wrote:
> 
> @XVincentX commented on this pull request.
> 
> In src/functions/schema.ts:
> 
> > @@ -14,6 +15,12 @@ ajv._opts.defaultMeta = jsonSpecv4.id;
>  // @ts-ignore
>  ajv._refs['http://json-schema.org/schema'] = 'http://json-schema.org/draft-04/schema';
>  
> +ajv.addFormat('int32', { type: 'number', validate: validator.int32 });
> +ajv.addFormat('int64', { type: 'number', validate: validator.int64 });
> +ajv.addFormat('float', { type: 'number', validate: validator.float });
> +ajv.addFormat('double', { type: 'number', validate: validator.double });
> +ajv.addFormat('byte', { type: 'string', validate: validator.byte });
> Hmm, ok. I fail to understand the difference with what was proposed last week though. It was the exact same solution.
> 
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub, or mute the thread.

marbemac: > I'd drop summary entirely.

I agree, summary doesn't make much sense on the validation results (still makes sense on the rule definition). Instead, consumers could retrieve summary on demand, as needed. We already expose `get rules()` on the spectral instance, so with no changes a consumer could do this:

```ts
const summary = spectralInstance.rules[result.rule].summary;
```

> I'm not sure whether it makes sense to include property given the fact path is already present.

Yeah I agree in general, but there is a tricky use case - how to describe a missing property. Because the property does not exist in the source, we have nothing to highlight / point to inside of Studio, for example. So we end up putting the error on the parent object, and having a bit more information about the missing property in the message is helpful, e.g. "the description property is missing" versus "a property is missing" (and having user look at the path to see which property is missing).

> I would like to see our summary generally be more useful, it should be a very quick hint as to what is going on, with a little information about the field or whatever. Those in the know will look at this short summary and know how to fix it, but others will want more information.

Maybe we should more formally define what the goal of `message` (property on each validation result) and `summary` (property on the rule itself) is? As it stands right now, message is generated at runtime, during validation, and summary is hardcoded into the rule itself and never changes.

> That said, I'm still open to making messages more customizable.
We could implement a decent string interpolation.

We could run messages through https://mustache.github.io, and expose several variables to it, so that rule authors can generate more useful messages?

> Literally putting everything through the os validator

I agree - we should improve the messages in spectral itself (which will benefit all downstream consumers, such as Studio), versus cleaning things up in the CLI (assuming I'm understanding this PR correctly).

P0lip: > Maybe we should more formally define what the goal of message (property on each validation result) and summary (property on the rule itself) is? As it stands right now, message is generated at runtime, during validation, and summary is hardcoded into the rule itself and never changes.

`summary` could be renamed to `description`. This would be less confusing.

> Yeah I agree in general, but there is a tricky use case - how to describe a missing property. Because the property does not exist in the source, we have nothing to highlight / point to inside of Studio, for example. So we end up putting the error on the parent object, and having a bit more information about the missing property in the message is helpful, e.g. "the description property is missing" versus "a property is missing" (and having user look at the path to see which property is missing).

This is indeed a valid case.

> We could run messages through https://mustache.github.io, and expose several variables to it, so that rule authors can generate more useful messages?

Frankly speaking, this what I wanted to initially implement in the scope of this PR.
I even implemented some code that should be sufficient to cover our needs.
```

P0lip: @chris-miaskowski you might want to take a look at this PR as well
philsturgeon: This PR is in response to https://stoplightio.atlassian.net/browse/SO-84 which is meant to formally define what summary and message are, because sometimes summary is useful and sometimes message is useful. Having the two has lead to inconsistencies.

I feel like we got to a good consensus here, lets drop summary entirely and just have message, which looks like this: 

![image](https://user-images.githubusercontent.com/67381/58030728-50b8c600-7b1f-11e9-8e22-eccb6471869c.png)

Basically exactly what you've done, but everywhere, for all the formatters. 

To avoid needing to make this a breaking change and therefore v3.0, we can just make summary an alias of message and mark it as deprecated in code, or something like that?  
P0lip: @philsturgeon 
Sounds like a good plan to me! Will do.
P0lip: @philsturgeon do you have any extra feedback or can I merge it?

@marbemac a few words on the package you mentioned - it seems truly great. I'm not sure what others think, but IMHO it might be a nice addition. Just wondering whether we should do it as a part of this PR or next one. Up to discussion.
marbemac: Totally cool to do a second pass and separate PR to make the messages even better in the future! Don't need to block this one.

Patch:
diff --git a/package.json b/package.json
index ce0cc3c9..e61ecb41 100644
--- a/package.json
+++ b/package.json
@@ -64,6 +64,7 @@
     "@stoplight/types": "^5.1.0",
     "@stoplight/yaml": "^2.3.0",
     "ajv": "6.x.x",
+    "ajv-oai": "^1.1.1",
     "chalk": "^2.4.2",
     "jsonpath": "https://github.com/stoplightio/jsonpath#f1c0e9e634da2d45671b7639fa0a99afc807da17",
     "lodash": ">=4.17.5",
diff --git a/src/formatters/stylish.ts b/src/formatters/stylish.ts
index acf20fae..4e7615cf 100644
--- a/src/formatters/stylish.ts
+++ b/src/formatters/stylish.ts
@@ -86,12 +86,7 @@ export const stylish = (results: IRuleResult[]): string => {
         messageType = chalk.yellow('white');
       }
 
-      return [
-        formatRange(result.range),
-        messageType,
-        result.code !== undefined ? result.code : '',
-        result.summary ? result.summary.replace(/([^ ])\.$/u, '$1') : result.message,
-      ];
+      return [formatRange(result.range), messageType, result.code !== undefined ? result.code : '', result.message];
     });
 
     output += `${table(pathTableData, {
diff --git a/src/functions/schema.ts b/src/functions/schema.ts
index dc487205..b5cc4e8c 100644
--- a/src/functions/schema.ts
+++ b/src/functions/schema.ts
@@ -1,6 +1,8 @@
 import { decodePointerFragment } from '@stoplight/json';
 import * as AJV from 'ajv';
 import * as jsonSpecv4 from 'ajv/lib/refs/json-schema-draft-04.json';
+const oasFormatValidator = require('ajv-oai/lib/format-validator');
+import { IFunction, IFunctionResult, ISchemaOptions } from '../types';
 
 const ajv = new AJV({
   meta: false,
@@ -14,7 +16,31 @@ ajv._opts.defaultMeta = jsonSpecv4.id;
 // @ts-ignore
 ajv._refs['http://json-schema.org/schema'] = 'http://json-schema.org/draft-04/schema';
 
-import { IFunction, IFunctionResult, ISchemaOptions } from '../types';
+ajv.addFormat('int32', { type: 'number', validate: oasFormatValidator.int32 });
+ajv.addFormat('int64', { type: 'number', validate: oasFormatValidator.int64 });
+ajv.addFormat('float', { type: 'number', validate: oasFormatValidator.float });
+ajv.addFormat('double', { type: 'number', validate: oasFormatValidator.double });
+ajv.addFormat('byte', { type: 'string', validate: oasFormatValidator.byte });
+
+const formatPath = (path: string) =>
+  path
+    .split('/')
+    .slice(1)
+    .map(decodePointerFragment);
+
+const mergeErrors = (existingError: IFunctionResult, newError: AJV.ErrorObject) => {
+  switch (newError.keyword) {
+    case 'additionalProperties': {
+      const { additionalProperty } = newError.params as AJV.AdditionalPropertiesParams;
+      if (!new RegExp(`[:,] ${additionalProperty}`).test(existingError.message)) {
+        existingError.message += `, ${(newError.params as AJV.AdditionalPropertiesParams).additionalProperty}`;
+      }
+      return true;
+    }
+    default:
+      return existingError.message === newError.message;
+  }
+};
 
 export const schema: IFunction<ISchemaOptions> = (targetVal, opts, paths) => {
   const results: IFunctionResult[] = [];
@@ -31,25 +57,33 @@ export const schema: IFunction<ISchemaOptions> = (targetVal, opts, paths) => {
 
   const { schema: schemaObj } = opts;
 
-  // TODO: potential performance improvements (compile, etc)?
   if (!ajv.validate(schemaObj, targetVal) && ajv.errors) {
-    ajv.errors.forEach((e: AJV.ErrorObject) => {
-      // @ts-ignore
-      if (e.params && e.params.additionalProperty) {
-        // @ts-ignore
-        e.message = e.message + ': ' + e.params.additionalProperty;
+    // TODO: potential performance improvements (compile, etc)?
+    const collectedErrors: string[] = [];
+
+    for (const error of ajv.errors) {
+      if (collectedErrors.length > 0) {
+        const index = collectedErrors.indexOf(error.keyword);
+        if (index !== -1) {
+          if (mergeErrors(results[index], error)) continue;
+        }
+      }
+
+      let message = error.message || '';
+
+      if (
+        error.keyword === 'additionalProperties' &&
+        (error.params as AJV.AdditionalPropertiesParams).additionalProperty
+      ) {
+        message += `: ${(error.params as AJV.AdditionalPropertiesParams).additionalProperty}`;
       }
 
+      collectedErrors.push(error.keyword);
       results.push({
-        path: path.concat(
-          e.dataPath
-            .split('/')
-            .slice(1)
-            .map(frag => decodePointerFragment(frag)),
-        ),
-        message: e.message ? e.message : '',
+        path: [...path, ...formatPath(error.dataPath)],
+        message,
       });
-    });
+    }
   }
 
   return results;
diff --git a/src/linter.ts b/src/linter.ts
index 1b793d6f..db65edef 100644
--- a/src/linter.ts
+++ b/src/linter.ts
@@ -88,13 +88,28 @@ export const lintNode = (
 
     results = results.concat(
       targetResults.map(result => {
-        const location = parsedResult.getLocationForJsonPath(parsed, result.path || targetPath, true);
+        const path = result.path || targetPath;
+        const location = parsedResult.getLocationForJsonPath(parsed, path, true);
 
         return {
           code: rule.name,
-          summary: rule.summary,
-          message: result.message,
-          path: result.path || targetPath,
+
+          // @deprecated, points to message
+          get summary() {
+            return this.message;
+          },
+
+          message:
+            rule.message === undefined
+              ? rule.summary || result.message
+              : typeof rule.message === 'function'
+                ? rule.message({
+                    error: result.message,
+                    property: path.length > 0 ? path[path.length - 1] : '',
+                    description: rule.description,
+                  })
+                : rule.message,
+          path,
           severity,
           source: parsedResult.source,
           ...(location || {
diff --git a/src/rulesets/message.ts b/src/rulesets/message.ts
new file mode 100644
index 00000000..e173561f
--- /dev/null
+++ b/src/rulesets/message.ts
@@ -0,0 +1,19 @@
+export interface IMessageVars {
+  property: string | number;
+  error: string;
+  description?: string;
+}
+
+export type MessageInterpolator = (values: IMessageVars) => string;
+
+export function message(strings: TemplateStringsArray, ...vars: Array<keyof IMessageVars>) {
+  return (values: IMessageVars) => {
+    const result = [strings[0]];
+
+    for (const [i, key] of vars.entries()) {
+      result.push(String(values[key] || ''), strings[i + 1]);
+    }
+
+    return result.join('');
+  };
+}
diff --git a/src/rulesets/oas2/index.ts b/src/rulesets/oas2/index.ts
index fed2713f..24d92c94 100644
--- a/src/rulesets/oas2/index.ts
+++ b/src/rulesets/oas2/index.ts
@@ -2,6 +2,7 @@ const merge = require('lodash/merge');
 
 import { DiagnosticSeverity } from '@stoplight/types';
 import { RuleFunction, RuleType } from '../../types';
+import { message } from '../message';
 import { commonOasRules } from '../oas';
 import * as schema from './schemas/main.json';
 
@@ -11,7 +12,7 @@ export const oas2Rules = () => {
   return merge(commonOasRules(), {
     // specification validation
     'oas2-schema': {
-      summary: 'Validate structure of OpenAPIv2 specification.',
+      message: message`${'error'}`,
       type: RuleType.VALIDATION,
       severity: DiagnosticSeverity.Error,
       then: {
@@ -25,7 +26,7 @@ export const oas2Rules = () => {
 
     // generic
     'api-host': {
-      summary: 'OpenAPI `host` must be present and non-empty string.',
+      message: 'OpenAPI `host` must be present and non-empty string.',
       type: RuleType.STYLE,
       given: '$',
       then: {
@@ -35,7 +36,7 @@ export const oas2Rules = () => {
       tags: ['api'],
     },
     'api-schemes': {
-      summary: 'OpenAPI host `schemes` must be present and non-empty array.',
+      message: 'OpenAPI host `schemes` must be present and non-empty array.',
       type: RuleType.STYLE,
       given: '$',
       then: {
@@ -55,7 +56,7 @@ export const oas2Rules = () => {
     },
     'host-not-example': {
       enabled: false,
-      summary: 'Server URL should not point at `example.com`.',
+      message: 'Server URL should not point at `example.com`.',
       type: RuleType.STYLE,
       given: '$',
       then: {
@@ -67,7 +68,7 @@ export const oas2Rules = () => {
       },
     },
     'host-trailing-slash': {
-      summary: 'Server URL should not have a trailing slash.',
+      message: 'Server URL should not have a trailing slash.',
       type: RuleType.STYLE,
       given: '$',
       then: {
@@ -80,7 +81,7 @@ export const oas2Rules = () => {
     },
     'model-description': {
       enabled: false,
-      summary: 'Definition `description` must be present and non-empty string.',
+      message: 'Definition `description` must be present and non-empty string.',
       type: RuleType.STYLE,
       given: '$..definitions[*]',
       then: {
@@ -89,7 +90,7 @@ export const oas2Rules = () => {
       },
     },
     'operation-security-defined': {
-      summary: 'Operation `security` values must match a scheme defined in the `securityDefinitions` object.',
+      message: 'Operation `security` values must match a scheme defined in the `securityDefinitions` object.',
       type: RuleType.VALIDATION,
       given: '$',
       then: {
@@ -101,7 +102,7 @@ export const oas2Rules = () => {
       tags: ['operation'],
     },
     'valid-example': {
-      summary: 'Examples must be valid against their defined schema.',
+      message: message`"${'property'}" property ${'error'}`,
       type: RuleType.VALIDATION,
       given: '$..[?(@.example)]',
       then: {
diff --git a/src/rulesets/oas3/index.ts b/src/rulesets/oas3/index.ts
index 11356fa8..43584b56 100644
--- a/src/rulesets/oas3/index.ts
+++ b/src/rulesets/oas3/index.ts
@@ -2,6 +2,7 @@ const merge = require('lodash/merge');
 
 import { DiagnosticSeverity } from '@stoplight/types';
 import { RuleFunction, RuleType } from '../../types';
+import { message } from '../message';
 import { commonOasRules } from '../oas';
 import * as schema from './schemas/main.json';
 
@@ -11,7 +12,7 @@ export const oas3Rules = () => {
   return merge(commonOasRules(), {
     // specification validation
     'oas3-schema': {
-      summary: 'Validate structure of OpenAPIv3 specification.',
+      message: message`${'error'}`,
       type: RuleType.VALIDATION,
       severity: DiagnosticSeverity.Error,
       then: {
@@ -25,7 +26,7 @@ export const oas3Rules = () => {
 
     // generic rules
     'api-servers': {
-      summary: 'OpenAPI `servers` must be present and non-empty array.',
+      message: 'OpenAPI `servers` must be present and non-empty array.',
       type: RuleType.STYLE,
       given: '$',
       then: {
@@ -45,7 +46,7 @@ export const oas3Rules = () => {
     },
     'model-description': {
       enabled: false,
-      summary: 'Model `description` must be present and non-empty string.',
+      message: 'Model `description` must be present and non-empty string.',
       type: RuleType.STYLE,
       given: '$.components.schemas[*]',
       then: {
@@ -54,7 +55,7 @@ export const oas3Rules = () => {
       },
     },
     'operation-security-defined': {
-      summary: 'Operation `security` values must match a scheme defined in the `components.securitySchemes` object.',
+      message: 'Operation `security` values must match a scheme defined in the `components.securitySchemes` object.',
       type: RuleType.VALIDATION,
       given: '$',
       then: {
@@ -67,7 +68,7 @@ export const oas3Rules = () => {
     },
     'server-not-example.com': {
       enabled: false,
-      summary: 'Server URL should not point at `example.com`.',
+      message: 'Server URL should not point at `example.com`.',
       type: RuleType.STYLE,
       given: '$.servers[*]',
       then: {
@@ -79,7 +80,7 @@ export const oas3Rules = () => {
       },
     },
     'server-trailing-slash': {
-      summary: 'Server URL should not have a trailing slash.',
+      message: 'Server URL should not have a trailing slash.',
       type: RuleType.STYLE,
       given: '$.servers[*]',
       then: {
@@ -91,7 +92,7 @@ export const oas3Rules = () => {
       },
     },
     'valid-example': {
-      summary: 'Examples must be valid against their defined schema.',
+      message: message`"${'property'}" property ${'error'}`,
       type: RuleType.VALIDATION,
       given: '$..[?(@.example)]',
       then: {
diff --git a/src/types/rule.ts b/src/types/rule.ts
index efc069bb..6e3a7d0f 100644
--- a/src/types/rule.ts
+++ b/src/types/rule.ts
@@ -1,4 +1,5 @@
 import { DiagnosticSeverity } from '@stoplight/types';
+import { MessageInterpolator } from '../rulesets/message';
 import { RuleFunction, RuleType } from './enums';
 
 export type Rule = IRule | TruthyRule | XorRule | LengthRule | AlphaRule | PatternRule | SchemaRule;
@@ -6,9 +7,12 @@ export type Rule = IRule | TruthyRule | XorRule | LengthRule | AlphaRule | Patte
 export interface IRule<T = string, O = any> {
   type?: RuleType;
 
-  // A short summary of the rule and its intended purpose
+  // @deprecated - use message instead
   summary?: string;
 
+  // A meaningful feedback about the error
+  message?: string | MessageInterpolator;
+
   // A long-form description of the rule formatted in markdown
   description?: string;
 
diff --git a/src/types/spectral.ts b/src/types/spectral.ts
index e73cffcd..745c00e4 100644
--- a/src/types/spectral.ts
+++ b/src/types/spectral.ts
@@ -34,6 +34,7 @@ export interface IRunOpts {
 }
 
 export interface IRuleResult extends IDiagnostic {
+  // @deprecated, use message instead
   summary?: string;
   path: JsonPath;
 }
diff --git a/yarn.lock b/yarn.lock
index 3d558dde..93b00f8a 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1052,7 +1052,15 @@ aggregate-error@^2.0.0:
     clean-stack "^2.0.0"
     indent-string "^3.0.0"
 
-ajv@6.x.x, ajv@^6.5.5:
+ajv-oai@^1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/ajv-oai/-/ajv-oai-1.1.1.tgz#5f3c19ebc6b1628465bc75436c1ade9031b3cab3"
+  integrity sha1-XzwZ68axYoRlvHVDbBrekDGzyrM=
+  dependencies:
+    ajv "^6.1.1"
+    decimal.js "^9.0.1"
+
+ajv@6.x.x, ajv@^6.1.1, ajv@^6.5.5:
   version "6.10.0"
   resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.10.0.tgz#90d0d54439da587cd7e843bfb7045f50bd22bdf1"
   integrity sha512-nffhOpkymDECQyR0mnsUtoCE8RlX38G0rYP+wgLWFyZuUyuuojSSvi/+euOiQBIn63whYwYVIIH1TvE3tu4OEg==
@@ -2340,6 +2348,11 @@ decamelize@^1.1.0, decamelize@^1.1.1, decamelize@^1.2.0:
   resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
   integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=
 
+decimal.js@^9.0.1:
+  version "9.0.1"
+  resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-9.0.1.tgz#1cc8b228177da7ab6498c1cc06eb130a290e6e1e"
+  integrity sha512-2h0iKbJwnImBk4TGk7CG1xadoA0g3LDPlQhQzbZ221zvG0p2YVUedbKIPsOZXKZGx6YmZMJKYOalpCMxSdDqTQ==
+
 decode-uri-component@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.0.tgz#eb3913333458775cb84cd1a1fae062106bb87545"


Test patch:
diff --git a/src/__tests__/__fixtures__/petstore.invalid-schema.oas3.yaml b/src/__tests__/__fixtures__/petstore.invalid-schema.oas3.yaml
new file mode 100644
index 00000000..ee2a0ca7
--- /dev/null
+++ b/src/__tests__/__fixtures__/petstore.invalid-schema.oas3.yaml
@@ -0,0 +1,58 @@
+openapi: 3.0.0
+info:
+  version: 1.0.0
+  title: Swagger Petstore
+  license:
+    name: MIT
+  contact:
+    email: bar@foo
+  description: test
+servers:
+  - url: 'http://petstore.swagger.io/v1'
+paths:
+  /pets:
+    get:
+      summary: List all pets
+      operationId: listPets
+      description: test
+      tags:
+        - pets
+      parameters:
+        - name: limit
+          in: query
+          description: How many items to return at one time (max 100)
+          required: false
+          schema:
+            type: integer
+            format: int32
+      responses:
+        '200':
+          description: A paged array of pets
+          headers:
+            x-next:
+              description: A link to the next page of responses
+              schema:
+                type: string
+            header-1:
+              type: string
+              op: foo
+          content:
+            application/json:
+              schema:
+                $ref: ./models/pet.yaml
+        default:
+          description: unexpected error
+          content:
+            application/json:
+              schema:
+                $ref: ../common/models/error.yaml
+components:
+  schemas:
+    Pets:
+      type: array
+      items:
+        $ref: ./models/pet.yaml
+      x-tags:
+        - Pets
+      title: Pets
+      description: A list of pets.
diff --git a/src/__tests__/linter.test.ts b/src/__tests__/linter.test.ts
index ca33ccea..a7670de1 100644
--- a/src/__tests__/linter.test.ts
+++ b/src/__tests__/linter.test.ts
@@ -1,8 +1,16 @@
 import { DiagnosticSeverity } from '@stoplight/types';
+import * as fs from 'fs';
+import * as path from 'path';
 
 import { oas2Functions, oas2Rules } from '../rulesets/oas2';
+import { oas3Functions, oas3Rules } from '../rulesets/oas3';
 import { Spectral } from '../spectral';
 
+const invalidSchema = fs.readFileSync(
+  path.join(__dirname, './__fixtures__/petstore.invalid-schema.oas3.yaml'),
+  'utf-8',
+);
+
 const fnName = 'fake';
 const fnName2 = 'fake2';
 const target = {
@@ -89,7 +97,7 @@ describe('linter', () => {
       },
     });
 
-    expect(result[0]).toEqual({
+    expect(result[0]).toMatchObject({
       code: 'rule1',
       message,
       severity: DiagnosticSeverity.Warning,
@@ -189,6 +197,34 @@ responses:: !!foo
     );
   });
 
+  test('should merge similar ajv errors', async () => {
+    spectral.addRules(oas3Rules());
+    spectral.addFunctions(oas3Functions());
+
+    const result = await spectral.run(invalidSchema);
+
+    expect(result).toEqual([
+      expect.objectContaining({
+        code: 'oas3-schema',
+        message: 'should NOT have additional properties: type',
+        summary: 'should NOT have additional properties: type',
+        path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1'],
+      }),
+      expect.objectContaining({
+        code: 'oas3-schema',
+        message: 'should match exactly one schema in oneOf',
+        summary: 'should match exactly one schema in oneOf',
+        path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1'],
+      }),
+      expect.objectContaining({
+        code: 'oas3-schema',
+        message: "should have required property '$ref'",
+        summary: "should have required property '$ref'",
+        path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1'],
+      }),
+    ]);
+  });
+
   describe('functional tests for the given property', () => {
     let fakeLintingFunction: any;
 
diff --git a/src/cli/commands/__tests__/lint.test.ts b/src/cli/commands/__tests__/lint.test.ts
index 12a8fe9f..39404944 100644
--- a/src/cli/commands/__tests__/lint.test.ts
+++ b/src/cli/commands/__tests__/lint.test.ts
@@ -57,7 +57,7 @@ describe('lint', () => {
           .command([...args, '-f', 'json'])
           .it('outputs warnings in json format', ctx => {
             expect(ctx.stdout).toContain('OpenAPI `servers` must be present and non-empty array');
-            expect(ctx.stdout).toContain('"info.contact is not truthy"');
+            expect(ctx.stdout).toContain('"Info object should contain `contact` object."');
           });
       });
 
@@ -243,9 +243,9 @@ describe('lint', () => {
       .stdout()
       .command(['lint', invalidSpecPath, '-c', validConfigPath])
       .it('outputs warnings in json format', ctx => {
-        expect(ctx.stdout).toContain('"info.contact is not truthy"');
-        expect(ctx.stdout).toContain('"info.description is not truthy"');
-        expect(ctx.stdout).toContain('"servers does not exist"');
+        expect(ctx.stdout).toContain('"Info object should contain `contact` object."');
+        expect(ctx.stdout).toContain('"OpenAPI object info `description` must be present and non-empty string."');
+        expect(ctx.stdout).toContain('"OpenAPI `servers` must be present and non-empty array."');
       });
 
     test
@@ -281,9 +281,9 @@ describe('lint', () => {
       .stdout()
       .command(['lint', invalidSpecPath, '-c', validConfigPath, '--max-results', '1'])
       .it('setting --max-results to 1 will override config value of 5', ctx => {
-        expect(ctx.stdout).toContain('"info.contact is not truthy"');
-        expect(ctx.stdout).not.toContain('"info.description is not truthy"');
-        expect(ctx.stdout).not.toContain('"servers does not exist"');
+        expect(ctx.stdout).toContain('"Info object should contain `contact` object."');
+        expect(ctx.stdout).not.toContain('"OpenAPI object info `description` must be present and non-empty string."');
+        expect(ctx.stdout).not.toContain('"OpenAPI `servers` must be present and non-empty array."');
       });
   });
 
@@ -307,7 +307,7 @@ describe('lint', () => {
       .stdout()
       .command(['lint', invalidSpecPath])
       .it('outputs data in format from default config file', ctx => {
-        expect(ctx.stdout).toContain('"info.contact is not truthy"');
+        expect(ctx.stdout).toContain('"Info object should contain `contact` object."');
       });
   });
 });
diff --git a/src/formatters/__tests__/stylish.test.ts b/src/formatters/__tests__/stylish.test.ts
new file mode 100644
index 00000000..79796c23
--- /dev/null
+++ b/src/formatters/__tests__/stylish.test.ts
@@ -0,0 +1,67 @@
+import { stylish } from '../stylish';
+
+const results = [
+  {
+    code: 'oas3-schema',
+    summary: 'Validate structure of OpenAPIv3 specification.',
+    message: 'should NOT have additional properties: type',
+    path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1'],
+    severity: 0,
+    source: '/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.invalid-schema.oas3.yaml',
+    range: {
+      start: {
+        line: 35,
+        character: 21,
+      },
+      end: {
+        line: 37,
+        character: 21,
+      },
+    },
+  },
+  {
+    code: 'oas3-schema',
+    summary: 'Validate structure of OpenAPIv3 specification.',
+    message: 'should match exactly one schema in oneOf',
+    path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1'],
+    severity: 0,
+    source: '/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.invalid-schema.oas3.yaml',
+    range: {
+      start: {
+        line: 35,
+        character: 21,
+      },
+      end: {
+        line: 37,
+        character: 21,
+      },
+    },
+  },
+  {
+    code: 'oas3-schema',
+    summary: 'Validate structure of OpenAPIv3 specification.',
+    message: "should have required property '$ref'",
+    path: ['paths', '/pets', 'get', 'responses', '200', 'headers', 'header-1'],
+    severity: 0,
+    source: '/home/Stoplight/spectral/src/__tests__/__fixtures__/petstore.invalid-schema.oas3.yaml',
+    range: {
+      start: {
+        line: 35,
+        character: 21,
+      },
+      end: {
+        line: 37,
+        character: 21,
+      },
+    },
+  },
+];
+
+describe('Stylish formatter', () => {
+  test('should prefer message for oas-schema errors', () => {
+    const result = stylish(results);
+    expect(result).toContain('oas3-schema  should NOT have additional properties: type\n');
+    expect(result).toContain('oas3-schema  should match exactly one schema in oneOf');
+    expect(result).toContain("oas3-schema  should have required property '$ref'\n");
+  });
+});
diff --git a/src/rulesets/oas/__tests__/__snapshots__/contact-properties.ts.snap b/src/rulesets/oas/__tests__/__snapshots__/contact-properties.ts.snap
index 0e7f1db7..a2e5fc4c 100644
--- a/src/rulesets/oas/__tests__/__snapshots__/contact-properties.ts.snap
+++ b/src/rulesets/oas/__tests__/__snapshots__/contact-properties.ts.snap
@@ -4,7 +4,7 @@ exports[`contact-properties return errors if name, url, email are missing 1`] =
 Array [
   Object {
     "code": "contact-properties",
-    "message": "info.contact.name is not truthy",
+    "message": "Contact object should have \`name\`, \`url\` and \`email\`.",
     "path": Array [
       "info",
       "contact",
@@ -26,7 +26,7 @@ Array [
   },
   Object {
     "code": "contact-properties",
-    "message": "info.contact.url is not truthy",
+    "message": "Contact object should have \`name\`, \`url\` and \`email\`.",
     "path": Array [
       "info",
       "contact",
@@ -48,7 +48,7 @@ Array [
   },
   Object {
     "code": "contact-properties",
-    "message": "info.contact.email is not truthy",
+    "message": "Contact object should have \`name\`, \`url\` and \`email\`.",
     "path": Array [
       "info",
       "contact",
diff --git a/src/rulesets/oas/__tests__/__snapshots__/example-value-or-externalValue.ts.snap b/src/rulesets/oas/__tests__/__snapshots__/example-value-or-externalValue.ts.snap
index d2cb14c0..8ffc6ed7 100644
--- a/src/rulesets/oas/__tests__/__snapshots__/example-value-or-externalValue.ts.snap
+++ b/src/rulesets/oas/__tests__/__snapshots__/example-value-or-externalValue.ts.snap
@@ -4,7 +4,7 @@ exports[`example-value-or-externalValue return errors if both externalValue and
 Array [
   Object {
     "code": "example-value-or-externalValue",
-    "message": "externalValue and value cannot both be defined",
+    "message": "Example should have either a \`value\` or \`externalValue\` field.",
     "path": Array [
       "example",
     ],
@@ -29,7 +29,7 @@ exports[`example-value-or-externalValue return errors if missing externalValue a
 Array [
   Object {
     "code": "example-value-or-externalValue",
-    "message": "externalValue and value cannot both be defined",
+    "message": "Example should have either a \`value\` or \`externalValue\` field.",
     "path": Array [
       "example",
     ],
diff --git a/src/rulesets/oas/__tests__/__snapshots__/info-contact.ts.snap b/src/rulesets/oas/__tests__/__snapshots__/info-contact.ts.snap
index 3c4bc7c3..20e3d654 100644
--- a/src/rulesets/oas/__tests__/__snapshots__/info-contact.ts.snap
+++ b/src/rulesets/oas/__tests__/__snapshots__/info-contact.ts.snap
@@ -4,7 +4,7 @@ exports[`info-contact return errors if info is missing contact 1`] = `
 Array [
   Object {
     "code": "info-contact",
-    "message": "info.contact is not truthy",
+    "message": "Info object should contain \`contact\` object.",
     "path": Array [
       "info",
       "contact",
diff --git a/src/rulesets/oas/__tests__/__snapshots__/info-description.ts.snap b/src/rulesets/oas/__tests__/__snapshots__/info-description.ts.snap
index c4952f64..c69b8387 100644
--- a/src/rulesets/oas/__tests__/__snapshots__/info-description.ts.snap
+++ b/src/rulesets/oas/__tests__/__snapshots__/info-description.ts.snap
@@ -4,7 +4,7 @@ exports[`info-description return errors if info missing description 1`] = `
 Array [
   Object {
     "code": "info-description",
-    "message": "info.description is not truthy",
+    "message": "OpenAPI object info \`description\` must be present and non-empty string.",
     "path": Array [
       "info",
       "description",
diff --git a/src/rulesets/oas/__tests__/__snapshots__/info-license.ts.snap b/src/rulesets/oas/__tests__/__snapshots__/info-license.ts.snap
index 445fa691..c0daaf99 100644
--- a/src/rulesets/oas/__tests__/__snapshots__/info-license.ts.snap
+++ b/src/rulesets/oas/__tests__/__snapshots__/info-license.ts.snap
@@ -4,7 +4,7 @@ exports[`info-license return errors if info missing license 1`] = `
 Array [
   Object {
     "code": "info-license",
-    "message": "info.license is not truthy",
+    "message": "OpenAPI object info \`license\` must be present and non-empty string.",
     "path": Array [
       "info",
       "license",
diff --git a/src/rulesets/oas/__tests__/__snapshots__/license-url.ts.snap b/src/rulesets/oas/__tests__/__snapshots__/license-url.ts.snap
index ec869d25..3a075a8d 100644
--- a/src/rulesets/oas/__tests__/__snapshots__/license-url.ts.snap
+++ b/src/rulesets/oas/__tests__/__snapshots__/license-url.ts.snap
@@ -4,7 +4,7 @@ exports[`license-url return errors if info license is missing url 1`] = `
 Array [
   Object {
     "code": "license-url",
-    "message": "info.license.url is not truthy",
+    "message": "License object should include \`url\`.",
     "path": Array [
       "info",
       "license",
diff --git a/src/rulesets/oas/__tests__/__snapshots__/no-eval-in-markdown.ts.snap b/src/rulesets/oas/__tests__/__snapshots__/no-eval-in-markdown.ts.snap
index c55e2b16..4e046707 100644
--- a/src/rulesets/oas/__tests__/__snapshots__/no-eval-in-markdown.ts.snap
+++ b/src/rulesets/oas/__tests__/__snapshots__/no-eval-in-markdown.ts.snap
@@ -4,7 +4,7 @@ exports[`no-eval-in-markdown return errors if descriptions or titles include eva
 Array [
   Object {
     "code": "no-eval-in-markdown",
-    "message": "must not match the pattern 'eval\\\\('",
+    "message": "Markdown descriptions should not contain \`eval(\`.",
     "path": Array [
       "info",
       "description",
@@ -25,7 +25,7 @@ Array [
   },
   Object {
     "code": "no-eval-in-markdown",
-    "message": "must not match the pattern 'eval\\\\('",
+    "message": "Markdown descriptions should not contain \`eval(\`.",
     "path": Array [
       "info",
       "title",
diff --git a/src/rulesets/oas/__tests__/__snapshots__/no-script-tags-in-markdown.ts.snap b/src/rulesets/oas/__tests__/__snapshots__/no-script-tags-in-markdown.ts.snap
index 3ff978e1..43da529a 100644
--- a/src/rulesets/oas/__tests__/__snapshots__/no-script-tags-in-markdown.ts.snap
+++ b/src/rulesets/oas/__tests__/__snapshots__/no-script-tags-in-markdown.ts.snap
@@ -4,7 +4,7 @@ exports[`no-script-tags-in-markdown return errors if descriptions include <scrip
 Array [
   Object {
     "code": "no-script-tags-in-markdown",
-    "message": "must not match the pattern '<script'",
+    "message": "Markdown descriptions should not contain \`<script>\` tags.",
     "path": Array [
       "info",
       "description",
diff --git a/src/rulesets/oas/__tests__/__snapshots__/only-local-references.ts.snap b/src/rulesets/oas/__tests__/__snapshots__/only-local-references.ts.snap
index b8fccc93..e96e443f 100644
--- a/src/rulesets/oas/__tests__/__snapshots__/only-local-references.ts.snap
+++ b/src/rulesets/oas/__tests__/__snapshots__/only-local-references.ts.snap
@@ -4,7 +4,7 @@ exports[`only-local-references return errors if not local ref 1`] = `
 Array [
   Object {
     "code": "only-local-references",
-    "message": "must match the pattern '^#\\\\/'",
+    "message": "References should start with \`#/\`.",
     "path": Array [
       "parameters",
       0,
diff --git a/src/rulesets/oas/__tests__/__snapshots__/openapi-tags-alphabetical.ts.snap b/src/rulesets/oas/__tests__/__snapshots__/openapi-tags-alphabetical.ts.snap
index acd6d045..c65fe416 100644
--- a/src/rulesets/oas/__tests__/__snapshots__/openapi-tags-alphabetical.ts.snap
+++ b/src/rulesets/oas/__tests__/__snapshots__/openapi-tags-alphabetical.ts.snap
@@ -4,7 +4,7 @@ exports[`openapi-tags-alphabetical return errors if tags is not in alphabetical
 Array [
   Object {
     "code": "openapi-tags-alphabetical",
-    "message": "properties are not in alphabetical order",
+    "message": "OpenAPI object should have alphabetical \`tags\`.",
     "path": Array [
       "tags",
     ],
diff --git a/src/rulesets/oas/__tests__/__snapshots__/openapi-tags.ts.snap b/src/rulesets/oas/__tests__/__snapshots__/openapi-tags.ts.snap
index 99da2800..0b07e0ff 100644
--- a/src/rulesets/oas/__tests__/__snapshots__/openapi-tags.ts.snap
+++ b/src/rulesets/oas/__tests__/__snapshots__/openapi-tags.ts.snap
@@ -4,7 +4,7 @@ exports[`openapi-tags return errors if missing tags 1`] = `
 Array [
   Object {
     "code": "openapi-tags",
-    "message": "tags is not truthy",
+    "message": "OpenAPI object should have non-empty \`tags\` array.",
     "path": Array [
       "tags",
     ],
diff --git a/src/rulesets/oas/__tests__/__snapshots__/operation-default-response.ts.snap b/src/rulesets/oas/__tests__/__snapshots__/operation-default-response.ts.snap
index 1fadff42..48c335b1 100644
--- a/src/rulesets/oas/__tests__/__snapshots__/operation-default-response.ts.snap
+++ b/src/rulesets/oas/__tests__/__snapshots__/operation-default-response.ts.snap
@@ -4,7 +4,7 @@ exports[`operation-default-response return errors if path-responses is missing d
 Array [
   Object {
     "code": "operation-default-response",
-    "message": "paths./path./get.responses.default is not truthy",
+    "message": "Operations must have a default response.",
     "path": Array [
       "paths",
       "/path",
diff --git a/src/rulesets/oas/__tests__/__snapshots__/operation-description.ts.snap b/src/rulesets/oas/__tests__/__snapshots__/operation-description.ts.snap
index 8eb07c82..30a2a730 100644
--- a/src/rulesets/oas/__tests__/__snapshots__/operation-description.ts.snap
+++ b/src/rulesets/oas/__tests__/__snapshots__/operation-description.ts.snap
@@ -6,7 +6,7 @@ exports[`operation-description return errors if operation description is missing
 Array [
   Object {
     "code": "operation-description",
-    "message": "paths./todos.get.description is not truthy",
+    "message": "Operation \`description\` must be present and non-empty string.",
     "path": Array [
       "paths",
       "/todos",
diff --git a/src/rulesets/oas/__tests__/__snapshots__/operation-operationId-valid-in-url.ts.snap b/src/rulesets/oas/__tests__/__snapshots__/operation-operationId-valid-in-url.ts.snap
index eb491ce7..d1b1b1f7 100644
--- a/src/rulesets/oas/__tests__/__snapshots__/operation-operationId-valid-in-url.ts.snap
+++ b/src/rulesets/oas/__tests__/__snapshots__/operation-operationId-valid-in-url.ts.snap
@@ -4,7 +4,7 @@ exports[`operation-operationId-valid-in-url return errors if operationId contain
 Array [
   Object {
     "code": "operation-operationId-valid-in-url",
-    "message": "must match the pattern '^[A-Za-z0-9-._~:/?#\\\\[\\\\]@!\\\\$&'()*+,;=]*$'",
+    "message": "operationId may only use characters that are valid when used in a URL.",
     "path": Array [
       "paths",
       "/todos",
diff --git a/src/rulesets/oas/__tests__/__snapshots__/operation-operationId.ts.snap b/src/rulesets/oas/__tests__/__snapshots__/operation-operationId.ts.snap
index ba18e2ca..16ba549c 100644
--- a/src/rulesets/oas/__tests__/__snapshots__/operation-operationId.ts.snap
+++ b/src/rulesets/oas/__tests__/__snapshots__/operation-operationId.ts.snap
@@ -6,7 +6,7 @@ exports[`operation-operationId return errors if operation id is missing 1`] = `
 Array [
   Object {
     "code": "operation-operationId",
-    "message": "paths./todos.get.operationId is not truthy",
+    "message": "Operation should have an \`operationId\`.",
     "path": Array [
       "paths",
       "/todos",
diff --git a/src/rulesets/oas/__tests__/__snapshots__/operation-singular-tag.ts.snap b/src/rulesets/oas/__tests__/__snapshots__/operation-singular-tag.ts.snap
index 34389359..a6e5cce6 100644
--- a/src/rulesets/oas/__tests__/__snapshots__/operation-singular-tag.ts.snap
+++ b/src/rulesets/oas/__tests__/__snapshots__/operation-singular-tag.ts.snap
@@ -4,7 +4,7 @@ exports[`operation-singular-tag return errors if tags has more than 1 1`] = `
 Array [
   Object {
     "code": "operation-singular-tag",
-    "message": "max length is 1",
+    "message": "Operation may only have one tag.",
     "path": Array [
       "paths",
       "/todos",
diff --git a/src/rulesets/oas/__tests__/__snapshots__/operation-summary-formatted.ts.snap b/src/rulesets/oas/__tests__/__snapshots__/operation-summary-formatted.ts.snap
index de8f9061..b69107fd 100644
--- a/src/rulesets/oas/__tests__/__snapshots__/operation-summary-formatted.ts.snap
+++ b/src/rulesets/oas/__tests__/__snapshots__/operation-summary-formatted.ts.snap
@@ -4,7 +4,7 @@ exports[`operation-summary-formatted return errors if summary does not end with
 Array [
   Object {
     "code": "operation-summary-formatted",
-    "message": "must match the pattern '^[A-Z].*\\\\.$'",
+    "message": "Operation \`summary\` should start with upper case and end with a dot.",
     "path": Array [
       "paths",
       "/todos",
@@ -32,7 +32,7 @@ exports[`operation-summary-formatted return errors if summary does not start wit
 Array [
   Object {
     "code": "operation-summary-formatted",
-    "message": "must match the pattern '^[A-Z].*\\\\.$'",
+    "message": "Operation \`summary\` should start with upper case and end with a dot.",
     "path": Array [
       "paths",
       "/todos",
diff --git a/src/rulesets/oas/__tests__/__snapshots__/operation-tags.ts.snap b/src/rulesets/oas/__tests__/__snapshots__/operation-tags.ts.snap
index ae13f75d..0cd020da 100644
--- a/src/rulesets/oas/__tests__/__snapshots__/operation-tags.ts.snap
+++ b/src/rulesets/oas/__tests__/__snapshots__/operation-tags.ts.snap
@@ -4,7 +4,7 @@ exports[`operation-tags return errors if tags is missing 1`] = `
 Array [
   Object {
     "code": "operation-tags",
-    "message": "paths./todos.get.tags is not truthy",
+    "message": "Operation should have non-empty \`tags\` array.",
     "path": Array [
       "paths",
       "/todos",
diff --git a/src/rulesets/oas/__tests__/__snapshots__/path-declarations-must-exist.ts.snap b/src/rulesets/oas/__tests__/__snapshots__/path-declarations-must-exist.ts.snap
index 357bc6e3..3eb094ad 100644
--- a/src/rulesets/oas/__tests__/__snapshots__/path-declarations-must-exist.ts.snap
+++ b/src/rulesets/oas/__tests__/__snapshots__/path-declarations-must-exist.ts.snap
@@ -4,7 +4,7 @@ exports[`path-declarations-must-exist return errors if parameter is empty 1`] =
 Array [
   Object {
     "code": "path-declarations-must-exist",
-    "message": "must not match the pattern '{}'",
+    "message": "given declarations cannot be empty, ex.\`/given/{}\` is invalid.",
     "path": Array [
       "paths",
       "/path/{}",
diff --git a/src/rulesets/oas/__tests__/__snapshots__/path-keys-no-trailing-slash.ts.snap b/src/rulesets/oas/__tests__/__snapshots__/path-keys-no-trailing-slash.ts.snap
index bf737955..c69153c0 100644
--- a/src/rulesets/oas/__tests__/__snapshots__/path-keys-no-trailing-slash.ts.snap
+++ b/src/rulesets/oas/__tests__/__snapshots__/path-keys-no-trailing-slash.ts.snap
@@ -4,7 +4,7 @@ exports[`path-keys-no-trailing-slash return errors if path ends with a slash 1`]
 Array [
   Object {
     "code": "path-keys-no-trailing-slash",
-    "message": "must not match the pattern '.+\\\\/$'",
+    "message": "given keys should not end with a slash.",
     "path": Array [
       "paths",
       "/path/",
diff --git a/src/rulesets/oas/__tests__/__snapshots__/path-not-include-query.ts.snap b/src/rulesets/oas/__tests__/__snapshots__/path-not-include-query.ts.snap
index a6c9ba98..32aada22 100644
--- a/src/rulesets/oas/__tests__/__snapshots__/path-not-include-query.ts.snap
+++ b/src/rulesets/oas/__tests__/__snapshots__/path-not-include-query.ts.snap
@@ -4,7 +4,7 @@ exports[`path-not-include-query return errors if includes a query 1`] = `
 Array [
   Object {
     "code": "path-not-include-query",
-    "message": "must not match the pattern '\\\\?'",
+    "message": "given keys should not include a query string.",
     "path": Array [
       "paths",
       "/path?query=true",
diff --git a/src/rulesets/oas/__tests__/__snapshots__/tag-description.ts.snap b/src/rulesets/oas/__tests__/__snapshots__/tag-description.ts.snap
index be614060..afd6113a 100644
--- a/src/rulesets/oas/__tests__/__snapshots__/tag-description.ts.snap
+++ b/src/rulesets/oas/__tests__/__snapshots__/tag-description.ts.snap
@@ -4,7 +4,7 @@ exports[`tag-description return errors if tag has no description 1`] = `
 Array [
   Object {
     "code": "tag-description",
-    "message": "tags.0.description is not truthy",
+    "message": "Tag object should have a \`description\`.",
     "path": Array [
       "tags",
       0,
diff --git a/src/rulesets/oas/__tests__/message.spec.ts b/src/rulesets/oas/__tests__/message.spec.ts
new file mode 100644
index 00000000..cb37f991
--- /dev/null
+++ b/src/rulesets/oas/__tests__/message.spec.ts
@@ -0,0 +1,13 @@
+import { message } from '../../message';
+
+describe('message util', () => {
+  test('interpolates correctly', () => {
+    const val = message`oops... ${'property'} missing;error: ${'error'}`;
+    expect(
+      val({
+        property: 'description',
+        error: 'expected property to be truthy',
+      }),
+    ).toEqual('oops... description missing;error: expected property to be truthy');
+  });
+});
diff --git a/src/rulesets/oas/__tests__/operation-2xx-response.ts b/src/rulesets/oas/__tests__/operation-2xx-response.ts
index 1c53e91d..198a908d 100644
--- a/src/rulesets/oas/__tests__/operation-2xx-response.ts
+++ b/src/rulesets/oas/__tests__/operation-2xx-response.ts
@@ -66,7 +66,7 @@ describe('operation-2xx-response', () => {
       expect(results).toEqual([
         expect.objectContaining({
           code: 'operation-2xx-response',
-          message: 'operations must define at least one 2xx response',
+          message: 'Operation must have at least one `2xx` response.',
           path: ['paths', '/path', method, 'responses'],
         }),
       ]);
@@ -90,7 +90,7 @@ describe('operation-2xx-response', () => {
     expect(results).toEqual([
       expect.objectContaining({
         code: 'operation-2xx-response',
-        message: 'operations must define at least one 2xx response',
+        summary: 'Operation must have at least one `2xx` response.',
         path: ['paths', '/path', 'get', 'responses'],
       }),
     ]);
diff --git a/src/rulesets/oas/__tests__/parameter-description.ts b/src/rulesets/oas/__tests__/parameter-description.ts
index 6cb6bda6..1b03c5b2 100644
--- a/src/rulesets/oas/__tests__/parameter-description.ts
+++ b/src/rulesets/oas/__tests__/parameter-description.ts
@@ -81,10 +81,9 @@ describe('parameter-description', () => {
     expect(results).toEqual([
       expect.objectContaining({
         code: 'parameter-description',
-        message: 'parameters.limit.description is not truthy',
+        message: 'Parameter objects should have a `description`.',
         path: ['parameters', 'limit', 'description'],
         severity: 1,
-        summary: 'Parameter objects should have a `description`.',
       }),
     ]);
   });
@@ -107,10 +106,9 @@ describe('parameter-description', () => {
     expect(results).toEqual([
       expect.objectContaining({
         code: 'parameter-description',
-        message: 'paths./todos.parameters.0.description is not truthy',
+        message: 'Parameter objects should have a `description`.',
         path: ['paths', '/todos', 'parameters', 0, 'description'],
         severity: 1,
-        summary: 'Parameter objects should have a `description`.',
       }),
     ]);
   });
@@ -135,10 +133,9 @@ describe('parameter-description', () => {
     expect(results).toEqual([
       expect.objectContaining({
         code: 'parameter-description',
-        message: 'paths./todos.get.parameters.0.description is not truthy',
+        message: 'Parameter objects should have a `description`.',
         path: ['paths', '/todos', 'get', 'parameters', 0, 'description'],
         severity: 1,
-        summary: 'Parameter objects should have a `description`.',
       }),
     ]);
   });
diff --git a/src/rulesets/oas/functions/oasOp2xxResponse/__tests__/__snapshots__/index.ts.snap b/src/rulesets/oas/functions/oasOp2xxResponse/__tests__/__snapshots__/index.ts.snap
index 5efa2891..72a1925f 100644
--- a/src/rulesets/oas/functions/oasOp2xxResponse/__tests__/__snapshots__/index.ts.snap
+++ b/src/rulesets/oas/functions/oasOp2xxResponse/__tests__/__snapshots__/index.ts.snap
@@ -4,7 +4,7 @@ exports[`oasOp2xxResponse return errors if missing 2xx 1`] = `
 Array [
   Object {
     "code": "operation-2xx-response",
-    "message": "operations must define at least one 2xx response",
+    "message": "Operation must have at least one \`2xx\` response.",
     "path": Array [
       "paths",
       "/path1",
@@ -32,7 +32,7 @@ exports[`oasOp2xxResponse return errors if no responses 1`] = `
 Array [
   Object {
     "code": "operation-2xx-response",
-    "message": "operations must define at least one 2xx response",
+    "message": "Operation must have at least one \`2xx\` response.",
     "path": Array [
       "paths",
       "/path1",
diff --git a/src/rulesets/oas/functions/oasOpFormDataConsumeCheck/__tests__/__snapshots__/index.ts.snap b/src/rulesets/oas/functions/oasOpFormDataConsumeCheck/__tests__/__snapshots__/index.ts.snap
index fd8fb9cb..e1e70231 100644
--- a/src/rulesets/oas/functions/oasOpFormDataConsumeCheck/__tests__/__snapshots__/index.ts.snap
+++ b/src/rulesets/oas/functions/oasOpFormDataConsumeCheck/__tests__/__snapshots__/index.ts.snap
@@ -4,7 +4,7 @@ exports[`oasOpFormDataConsumeCheck return errors on different path operations sa
 Array [
   Object {
     "code": "operation-formData-consume-check",
-    "message": "consumes must include urlencoded, multipart, or formdata media type when using formData parameter",
+    "message": "Operations with an \`in: formData\` parameter must include \`application/x-www-form-urlencoded\` or \`multipart/form-data\` in their \`consumes\` property.",
     "path": Array [
       "paths",
       "/path1",
diff --git a/src/rulesets/oas/functions/oasOpIdUnique/__tests__/__snapshots__/index.ts.snap b/src/rulesets/oas/functions/oasOpIdUnique/__tests__/__snapshots__/index.ts.snap
index df585a60..fcd7da9f 100644
--- a/src/rulesets/oas/functions/oasOpIdUnique/__tests__/__snapshots__/index.ts.snap
+++ b/src/rulesets/oas/functions/oasOpIdUnique/__tests__/__snapshots__/index.ts.snap
@@ -4,7 +4,7 @@ exports[`oasOpIdUnique return errors on different path operations same id 1`] =
 Array [
   Object {
     "code": "operation-operationId-unique",
-    "message": "operationId must be unique",
+    "message": "Every operation must have a unique \`operationId\`.",
     "path": Array [
       "paths",
       "/path1",
@@ -27,7 +27,7 @@ Array [
   },
   Object {
     "code": "operation-operationId-unique",
-    "message": "operationId must be unique",
+    "message": "Every operation must have a unique \`operationId\`.",
     "path": Array [
       "paths",
       "/path2",
@@ -55,7 +55,7 @@ exports[`oasOpIdUnique return errors on same path operations same id 1`] = `
 Array [
   Object {
     "code": "operation-operationId-unique",
-    "message": "operationId must be unique",
+    "message": "Every operation must have a unique \`operationId\`.",
     "path": Array [
       "paths",
       "/path1",
@@ -78,7 +78,7 @@ Array [
   },
   Object {
     "code": "operation-operationId-unique",
-    "message": "operationId must be unique",
+    "message": "Every operation must have a unique \`operationId\`.",
     "path": Array [
       "paths",
       "/path1",
diff --git a/src/rulesets/oas/functions/oasOpParams/__tests__/__snapshots__/index.test.ts.snap b/src/rulesets/oas/functions/oasOpParams/__tests__/__snapshots__/index.test.ts.snap
index e14833b9..33b3d86c 100644
--- a/src/rulesets/oas/functions/oasOpParams/__tests__/__snapshots__/index.test.ts.snap
+++ b/src/rulesets/oas/functions/oasOpParams/__tests__/__snapshots__/index.test.ts.snap
@@ -4,12 +4,7 @@ exports[`oasOpParams Error if both in:formData and in:body 1`] = `
 Array [
   Object {
     "code": "operation-parameters",
-    "message": "Operation cannot have both \`in:body\` and \`in:formData\` parameters.
-
-Parameters found at:
-[ \`paths\`, \`/foo\`, \`get\`, \`0\` ]
-[ \`paths\`, \`/foo\`, \`get\`, \`1\` ]
-",
+    "message": "Operation parameters are unique and non-repeating.",
     "path": Array [
       "paths",
       "/foo",
@@ -36,10 +31,7 @@ exports[`oasOpParams Error if multiple in:body 1`] = `
 Array [
   Object {
     "code": "operation-parameters",
-    "message": "Operation has multiple instances of the \`in:body\` parameter.
-
-Parameters found at:
-",
+    "message": "Operation parameters are unique and non-repeating.",
     "path": Array [
       "paths",
       "/foo",
@@ -66,14 +58,7 @@ exports[`oasOpParams Error if non-unique $ref param on same operation 1`] = `
 Array [
   Object {
     "code": "operation-parameters",
-    "message": "Operations must have unique \`name\` + \`in\` parameters.
-Repeats of \`in:query\` + \`name:foo\`
-
-Parameters found at:
-[ \`paths\`, \`/foo\`, \`get\`, \`0\` ]
-[ \`paths\`, \`/foo\`, \`get\`, \`1\` ]
-[ \`paths\`, \`/foo\`, \`get\`, \`2\` ]
-",
+    "message": "Operation parameters are unique and non-repeating.",
     "path": Array [
       "paths",
       "/foo",
@@ -100,14 +85,7 @@ exports[`oasOpParams Error if non-unique param on same operation 1`] = `
 Array [
   Object {
     "code": "operation-parameters",
-    "message": "Operations must have unique \`name\` + \`in\` parameters.
-Repeats of \`in:query\` + \`name:foo\`
-
-Parameters found at:
-[ \`paths\`, \`/foo\`, \`get\`, \`0\` ]
-[ \`paths\`, \`/foo\`, \`get\`, \`1\` ]
-[ \`paths\`, \`/foo\`, \`get\`, \`2\` ]
-",
+    "message": "Operation parameters are unique and non-repeating.",
     "path": Array [
       "paths",
       "/foo",
diff --git a/src/rulesets/oas/functions/oasOpSecurityDefined/__tests__/__snapshots__/index.ts.snap b/src/rulesets/oas/functions/oasOpSecurityDefined/__tests__/__snapshots__/index.ts.snap
index 0902cfcd..c207bd7e 100644
--- a/src/rulesets/oas/functions/oasOpSecurityDefined/__tests__/__snapshots__/index.ts.snap
+++ b/src/rulesets/oas/functions/oasOpSecurityDefined/__tests__/__snapshots__/index.ts.snap
@@ -4,7 +4,7 @@ exports[`oasOpSecurityDefined oas2 return errors on invalid object 1`] = `
 Array [
   Object {
     "code": "operation-security-defined",
-    "message": "operation referencing undefined security scheme",
+    "message": "Operation \`security\` values must match a scheme defined in the \`securityDefinitions\` object.",
     "path": Array [
       "paths",
       "/path",
@@ -33,7 +33,7 @@ exports[`oasOpSecurityDefined oas3 return errors on invalid object 1`] = `
 Array [
   Object {
     "code": "operation-security-defined",
-    "message": "operation referencing undefined security scheme",
+    "message": "Operation \`security\` values must match a scheme defined in the \`components.securitySchemes\` object.",
     "path": Array [
       "paths",
       "/path",
diff --git a/src/rulesets/oas/functions/oasPathParam/__tests__/__snapshots__/index.test.ts.snap b/src/rulesets/oas/functions/oasPathParam/__tests__/__snapshots__/index.test.ts.snap
index f83eeee5..4ce02cfa 100644
--- a/src/rulesets/oas/functions/oasPathParam/__tests__/__snapshots__/index.test.ts.snap
+++ b/src/rulesets/oas/functions/oasPathParam/__tests__/__snapshots__/index.test.ts.snap
@@ -4,9 +4,7 @@ exports[`oasPathParam Error if $ref path parameter definition is not required 1`
 Array [
   Object {
     "code": "path-params",
-    "message": "Path parameter \\"**bar**\\" must have a \`required\` that is set to \`true\`.
-
-To fix, mark this parameter as required.",
+    "message": "Path parameters are correct and valid.",
     "path": Array [
       "paths",
       "/foo/{bar}",
@@ -33,11 +31,7 @@ exports[`oasPathParam Error if duplicate path parameters with same name are used
 Array [
   Object {
     "code": "path-params",
-    "message": "The path \\"**/foo/{bar}/{bar}**\\" uses the parameter \\"**{bar}**\\" multiple times.
-
-Path parameters must be unique.
-
-To fix, update the path so that all parameter names are unique.",
+    "message": "Path parameters are correct and valid.",
     "path": Array [
       "paths",
       "/foo/{bar}/{bar}",
@@ -63,9 +57,7 @@ exports[`oasPathParam Error if no path parameter definition 1`] = `
 Array [
   Object {
     "code": "path-params",
-    "message": "The path \\"**/foo/{bar}**\\" uses a parameter \\"**{bar}**\\" that does not have a corresponding definition.
-
-To fix, add a path parameter with the name \\"**bar**\\".",
+    "message": "Path parameters are correct and valid.",
     "path": Array [
       "paths",
       "/foo/{bar}",
@@ -91,9 +83,7 @@ exports[`oasPathParam Error if paths are functionally equivalent 1`] = `
 Array [
   Object {
     "code": "path-params",
-    "message": "The paths \\"**/foo/{boo}**\\" and \\"**/foo/{bar}**\\" are equivalent.
-
-To fix, remove one of the paths or merge them together.",
+    "message": "Path parameters are correct and valid.",
     "path": Array [
       "paths",
     ],
diff --git a/src/rulesets/oas/functions/oasPathParam/__tests__/index.test.ts b/src/rulesets/oas/functions/oasPathParam/__tests__/index.test.ts
index 74f25858..4aea0258 100644
--- a/src/rulesets/oas/functions/oasPathParam/__tests__/index.test.ts
+++ b/src/rulesets/oas/functions/oasPathParam/__tests__/index.test.ts
@@ -34,7 +34,7 @@ describe('oasPathParam', () => {
     expect(results).toMatchSnapshot();
 
     expect(results[0].path).toEqual(['paths', '/foo/{bar}']);
-    expect(results[0].message).toContain('bar');
+    expect(results[0].message).toEqual('Path parameters are correct and valid.');
   });
 
   test('No error if path parameter definition is used (at the path level)', async () => {
@@ -115,7 +115,7 @@ describe('oasPathParam', () => {
     expect(results).toMatchSnapshot();
 
     expect(results[0].path).toEqual(['paths', '/foo/{bar}/{bar}']);
-    expect(results[0].message).toContain('bar');
+    expect(results[0].message).toEqual('Path parameters are correct and valid.');
   });
 
   test('Error if $ref path parameter definition is not required', async () => {
diff --git a/src/rulesets/oas2/__tests__/__snapshots__/api-host.ts.snap b/src/rulesets/oas2/__tests__/__snapshots__/api-host.ts.snap
index 30902886..9ade4188 100644
--- a/src/rulesets/oas2/__tests__/__snapshots__/api-host.ts.snap
+++ b/src/rulesets/oas2/__tests__/__snapshots__/api-host.ts.snap
@@ -4,7 +4,7 @@ exports[`api-host return errors if missing host 1`] = `
 Array [
   Object {
     "code": "api-host",
-    "message": "host is not truthy",
+    "message": "OpenAPI \`host\` must be present and non-empty string.",
     "path": Array [
       "host",
     ],
diff --git a/src/rulesets/oas2/__tests__/__snapshots__/api-schemes.ts.snap b/src/rulesets/oas2/__tests__/__snapshots__/api-schemes.ts.snap
index 2a55d384..8420abe0 100644
--- a/src/rulesets/oas2/__tests__/__snapshots__/api-schemes.ts.snap
+++ b/src/rulesets/oas2/__tests__/__snapshots__/api-schemes.ts.snap
@@ -4,7 +4,7 @@ exports[`api-schemes return errors if schemes is an empty array  1`] = `
 Array [
   Object {
     "code": "api-schemes",
-    "message": "should NOT have fewer than 1 items",
+    "message": "OpenAPI host \`schemes\` must be present and non-empty array.",
     "path": Array [
       "schemes",
     ],
@@ -29,7 +29,7 @@ exports[`api-schemes return errors if schemes is missing  1`] = `
 Array [
   Object {
     "code": "api-schemes",
-    "message": "schemes does not exist",
+    "message": "OpenAPI host \`schemes\` must be present and non-empty array.",
     "path": Array [
       "schemes",
     ],
diff --git a/src/rulesets/oas2/__tests__/__snapshots__/host-not-example.ts.snap b/src/rulesets/oas2/__tests__/__snapshots__/host-not-example.ts.snap
index 38168e9f..e330b355 100644
--- a/src/rulesets/oas2/__tests__/__snapshots__/host-not-example.ts.snap
+++ b/src/rulesets/oas2/__tests__/__snapshots__/host-not-example.ts.snap
@@ -4,7 +4,7 @@ exports[`host-not-example return errors if server is example.com 1`] = `
 Array [
   Object {
     "code": "host-not-example",
-    "message": "must not match the pattern 'example.com'",
+    "message": "Server URL should not point at \`example.com\`.",
     "path": Array [
       "host",
     ],
diff --git a/src/rulesets/oas2/__tests__/__snapshots__/host-trailing-slash.ts.snap b/src/rulesets/oas2/__tests__/__snapshots__/host-trailing-slash.ts.snap
index 0a9b21c1..807c24e9 100644
--- a/src/rulesets/oas2/__tests__/__snapshots__/host-trailing-slash.ts.snap
+++ b/src/rulesets/oas2/__tests__/__snapshots__/host-trailing-slash.ts.snap
@@ -4,7 +4,7 @@ exports[`host-trailing-slash return errors if host url ends with a slash 1`] = `
 Array [
   Object {
     "code": "host-trailing-slash",
-    "message": "must not match the pattern '/$'",
+    "message": "Server URL should not have a trailing slash.",
     "path": Array [
       "host",
     ],
diff --git a/src/rulesets/oas2/__tests__/__snapshots__/model-description.ts.snap b/src/rulesets/oas2/__tests__/__snapshots__/model-description.ts.snap
index 497e3f5d..314ba205 100644
--- a/src/rulesets/oas2/__tests__/__snapshots__/model-description.ts.snap
+++ b/src/rulesets/oas2/__tests__/__snapshots__/model-description.ts.snap
@@ -4,7 +4,7 @@ exports[`model-description return errors if a definition is missing description
 Array [
   Object {
     "code": "model-description",
-    "message": "definitions.user.description is not truthy",
+    "message": "Definition \`description\` must be present and non-empty string.",
     "path": Array [
       "definitions",
       "user",
diff --git a/src/rulesets/oas2/__tests__/valid-example.ts b/src/rulesets/oas2/__tests__/valid-example.ts
index 8f143b55..d334e8e1 100644
--- a/src/rulesets/oas2/__tests__/valid-example.ts
+++ b/src/rulesets/oas2/__tests__/valid-example.ts
@@ -155,7 +155,7 @@ describe('valid-example', () => {
     expect(results).toEqual([
       expect.objectContaining({
         code: 'valid-example',
-        summary: 'Examples must be valid against their defined schema.',
+        message: '"self" property should be array',
         path: ['definitions', 'halRoot', '_links', 'self'],
       }),
     ]);
@@ -220,40 +220,40 @@ describe('valid-example', () => {
     });
 
     expect(results).toMatchInlineSnapshot(`
-Array [
-  Object {
-    "code": "valid-example",
-    "message": "should be string",
-    "path": Array [
-      "paths",
-      "/pet",
-      "post",
-      "parameters",
-      0,
-      "schema",
-      "properties",
-      "a",
-      "properties",
-      "b",
-      "properties",
-      "c",
-    ],
-    "range": Object {
-      "end": Object {
-        "character": 42,
-        "line": 37,
-      },
-      "start": Object {
-        "character": 28,
-        "line": 35,
-      },
-    },
-    "severity": 1,
-    "source": undefined,
-    "summary": "Examples must be valid against their defined schema.",
-  },
-]
-`);
+      Array [
+        Object {
+          "code": "valid-example",
+          "message": "\\"c\\" property should be string",
+          "path": Array [
+            "paths",
+            "/pet",
+            "post",
+            "parameters",
+            0,
+            "schema",
+            "properties",
+            "a",
+            "properties",
+            "b",
+            "properties",
+            "c",
+          ],
+          "range": Object {
+            "end": Object {
+              "character": 42,
+              "line": 37,
+            },
+            "start": Object {
+              "character": 28,
+              "line": 35,
+            },
+          },
+          "severity": 1,
+          "source": undefined,
+          "summary": "\\"c\\" property should be string",
+        },
+      ]
+    `);
   });
 
   test('will not fail if an actual property is called example', async () => {
diff --git a/src/rulesets/oas3/__tests__/__snapshots__/api-servers.ts.snap b/src/rulesets/oas3/__tests__/__snapshots__/api-servers.ts.snap
index d5dd7264..090e2cda 100644
--- a/src/rulesets/oas3/__tests__/__snapshots__/api-servers.ts.snap
+++ b/src/rulesets/oas3/__tests__/__snapshots__/api-servers.ts.snap
@@ -4,7 +4,7 @@ exports[`api-servers return errors if servers is an empty array  1`] = `
 Array [
   Object {
     "code": "api-servers",
-    "message": "should NOT have fewer than 1 items",
+    "message": "OpenAPI \`servers\` must be present and non-empty array.",
     "path": Array [
       "servers",
     ],
@@ -29,7 +29,7 @@ exports[`api-servers return errors if servers is missing  1`] = `
 Array [
   Object {
     "code": "api-servers",
-    "message": "servers does not exist",
+    "message": "OpenAPI \`servers\` must be present and non-empty array.",
     "path": Array [
       "servers",
     ],
diff --git a/src/rulesets/oas3/__tests__/__snapshots__/model-description.ts.snap b/src/rulesets/oas3/__tests__/__snapshots__/model-description.ts.snap
index 9c547dc8..23c9596d 100644
--- a/src/rulesets/oas3/__tests__/__snapshots__/model-description.ts.snap
+++ b/src/rulesets/oas3/__tests__/__snapshots__/model-description.ts.snap
@@ -4,7 +4,7 @@ exports[`model-description return errors if a definition is missing description
 Array [
   Object {
     "code": "model-description",
-    "message": "components.schemas.user.description is not truthy",
+    "message": "Model \`description\` must be present and non-empty string.",
     "path": Array [
       "components",
       "schemas",
diff --git a/src/rulesets/oas3/__tests__/__snapshots__/server-not-example.ts.snap b/src/rulesets/oas3/__tests__/__snapshots__/server-not-example.ts.snap
index 4addce96..09e7e01c 100644
--- a/src/rulesets/oas3/__tests__/__snapshots__/server-not-example.ts.snap
+++ b/src/rulesets/oas3/__tests__/__snapshots__/server-not-example.ts.snap
@@ -4,7 +4,7 @@ exports[`server-not-example.com return errors if server is example.com 1`] = `
 Array [
   Object {
     "code": "server-not-example.com",
-    "message": "must not match the pattern 'example.com'",
+    "message": "Server URL should not point at \`example.com\`.",
     "path": Array [
       "servers",
       0,
diff --git a/src/rulesets/oas3/__tests__/__snapshots__/server-trailing-slash.ts.snap b/src/rulesets/oas3/__tests__/__snapshots__/server-trailing-slash.ts.snap
index 727c4908..319a5481 100644
--- a/src/rulesets/oas3/__tests__/__snapshots__/server-trailing-slash.ts.snap
+++ b/src/rulesets/oas3/__tests__/__snapshots__/server-trailing-slash.ts.snap
@@ -4,7 +4,7 @@ exports[`server-trailing-slash return errors if server url ends with a slash 1`]
 Array [
   Object {
     "code": "server-trailing-slash",
-    "message": "must not match the pattern '/$'",
+    "message": "Server URL should not have a trailing slash.",
     "path": Array [
       "servers",
       0,
diff --git a/src/rulesets/oas3/__tests__/valid-example.ts b/src/rulesets/oas3/__tests__/valid-example.ts
index d91d2ef5..1e4aac6d 100644
--- a/src/rulesets/oas3/__tests__/valid-example.ts
+++ b/src/rulesets/oas3/__tests__/valid-example.ts
@@ -3,6 +3,10 @@ import { oas3Rules } from '../index';
 
 const ruleset = { rules: oas3Rules() };
 
+// @oclif/test packages requires @types/mocha, therefore we have 2 packages coming up with similar typings
+// TS is confused and prefers the mocha ones, so we need to instrument it to pick up the Jest ones
+declare var test: jest.It;
+
 describe('valid-example', () => {
   const s = new Spectral();
   s.addRules({
@@ -119,47 +123,37 @@ describe('valid-example', () => {
     });
 
     expect(results).toMatchInlineSnapshot(`
-Array [
-  Object {
-    "code": "valid-example",
-    "message": "should match format \\"email\\"",
-    "path": Array [
-      "paths",
-      "/pet",
-      "post",
-      "requestBody",
-      "content",
-      "*/*",
-      "schema",
-    ],
-    "range": Object {
-      "end": Object {
-        "character": 34,
-        "line": 15,
-      },
-      "start": Object {
-        "character": 23,
-        "line": 12,
-      },
-    },
-    "severity": 1,
-    "source": undefined,
-    "summary": "Examples must be valid against their defined schema.",
-  },
-]
-`);
+      Array [
+        Object {
+          "code": "valid-example",
+          "message": "\\"schema\\" property should match format \\"email\\"",
+          "path": Array [
+            "paths",
+            "/pet",
+            "post",
+            "requestBody",
+            "content",
+            "*/*",
+            "schema",
+          ],
+          "range": Object {
+            "end": Object {
+              "character": 34,
+              "line": 15,
+            },
+            "start": Object {
+              "character": 23,
+              "line": 12,
+            },
+          },
+          "severity": 1,
+          "source": undefined,
+          "summary": "\\"schema\\" property should match format \\"email\\"",
+        },
+      ]
+    `);
   });
 
-  /*
-   * NOTE: This used to throw and error so we told AJV to ignore unknownFormats
-   *
-   *   Error: unknown format "int64" is used in schema at path "#"
-   *
-   * If this test starts to correctly report that 2886989840 is not a valid int64, then
-   * great, somebody must have trained AJV what a int64 is.
-   *
-   * More information: https://github.com/stoplightio/spectral/issues/187
-   */
   test('does not report example mismatches for unknown AJV formats', async () => {
     const results = await s.run({
       xoxo: {
@@ -167,7 +161,7 @@ Array [
         properties: {
           ip_address: {
             type: 'integer',
-            format: 'int64',
+            format: 'foo',
             example: 2886989840,
           },
         },
@@ -176,4 +170,49 @@ Array [
 
     expect(results).toEqual([]);
   });
+
+  test.each([['byte', '1'], ['int32', 2 ** 31], ['int64', 2 ** 63], ['float', 2 ** 128]])(
+    'reports invalid usage of %s format',
+    async (format, example) => {
+      const results = await s.run({
+        xoxo: {
+          type: 'object',
+          properties: {
+            ip_address: {
+              type: ['string', 'number'],
+              format,
+              example,
+            },
+          },
+        },
+      });
+
+      expect(results).toEqual([
+        expect.objectContaining({
+          code: 'valid-example',
+          message: `"ip_address" property should match format "${format}"`,
+        }),
+      ]);
+    },
+  );
+
+  test.each([['byte', 'MTI3'], ['int32', 2 ** 30], ['int64', 2 ** 40], ['float', 2 ** 64], ['double', 2 ** 1028]])(
+    'does not report valid usage of %s format',
+    async (format, example) => {
+      const results = await s.run({
+        xoxo: {
+          type: 'object',
+          properties: {
+            ip_address: {
+              type: ['string', 'number'],
+              format,
+              example,
+            },
+          },
+        },
+      });
+
+      expect(results).toHaveLength(0);
+    },
+  );
 });