This is a contract-testing tool: where we can't enforce a constraint precisely, we prefer a loud failure or an explicit "skipped" outcome over silently accepting non-compliant data. The list below pins down what does and does not get checked so you can decide whether the gaps matter for your spec.
- OpenAPI 3.0 vs 3.1
- Body validation
- Parameter styles
- Security schemes
- Schema features
- HTTP methods
- Spec features not consulted
- Warning channel (
E_USER_WARNINGcontract)
The package auto-detects the OAS version from the openapi field and handles schema conversion accordingly:
| Feature | 3.0 handling | 3.1 handling |
|---|---|---|
nullable: true |
Converted to type array ["string", "null"]; null appended to enum if present |
Not applicable (uses type arrays natively) |
prefixItems |
N/A | Converted to items array (Draft 07 tuple) |
$dynamicRef / $dynamicAnchor |
N/A | Removed (not in Draft 07) |
examples (array) |
Removed (Draft 2020-12 keyword, not Draft 07) | Removed (Draft 2020-12 keyword, not Draft 07) |
const |
N/A | Lowered to enum: [value] so opis Draft 07 enforces it |
readOnly / writeOnly |
Semantic enforcement (see below). Forbidden properties become boolean false subschemas; the keyword is dropped as OAS-only on surviving properties |
Semantic enforcement (see below). Forbidden properties become boolean false subschemas; the keyword is preserved on surviving properties (valid in Draft 07) |
Both validators apply OpenAPI's asymmetric semantics instead of letting the keywords pass as no-ops:
- Response validation (
OpenApiResponseValidator, Laravel trait): any property markedwriteOnly: truemust not appear in the response body. If it does, validation fails with the offending property named in the error. AwriteOnly + requiredentry is treated as absent on the response side, so a compliant response that omits the property still validates. - Request validation (
OpenApiRequestValidator): any property markedreadOnly: truemust not appear in the request body.readOnly + requiredis treated as absent on the request side, so a compliant request that omits the property still validates.
Detection looks at each property schema's own top-level readOnly / writeOnly; markers nested inside the property's allOf / oneOf / anyOf children are not enforced in the current release.
- Validated:
application/jsonand any+jsonstructured-syntax suffix (RFC 6838), and content keys using ranges (application/*,*/*) — the matcher tries exact match first, then<type>/*, then*/*. - Multi-JSON-per-status specs (e.g.
application/json+application/problem+jsonfor the same status): when the actual response Content-Type is supplied, schema validation prefers the spec key that exactly matches the response Content-Type before falling back to the first JSON key. A problem-details body served asapplication/problem+jsonis judged against its own schema, not the success-shapeapplication/jsonschema. Vendor+jsonsuffixes the spec doesn't enumerate (e.g.application/vnd.example.v1+json) still fall through to the first JSON key, preserving the legacy interchangeable-JSON behaviour for that case. - Presence-only (no schema validation): every other media type, including
application/xml,multipart/form-data,application/x-www-form-urlencoded,text/plain, andapplication/octet-stream. The validator confirms the spec declares the content type but does not check the body. When the matched media-type entry declares aschema(OpenAPI permits a schema on any media type, but this JSON Schema engine cannot evaluate a non-JSON one), the orchestrator marks the response/request asSkippedwith askipReasonso the unvalidated body is surfaced in coverage rather than counted as a clean pass. A non-JSON entry with noschemahas nothing to validate and stays a plain success. - Multipart
encodingobject: per-partcontentType/headers/style/explodeare not consulted. - Cascading
additionalProperties: falseerrors are stripped automatically. opis'sPropertiesKeywordskips itsaddCheckedProperties()call whenever any sub-property fails its schema, leaving$checkedempty in the validation context. The follow-onadditionalProperties: falsekeyword then reports every property the data carries — including ones explicitly declared in the schema'sproperties— as "additional". The validator walks opis'sValidationErrortree, reads the raw list of "additional" property names fromargs()['properties'], and filters out names that ARE declared in the schema'spropertieskeyword at that path. A single failure shows as one error, not a paired pseudo-error naming declared properties as not-allowed. Genuine additional properties still surface; mixed cases keep only the real extras in the message. The property-name comparison is fully structural (raw arrays + raw path segments — no string parsing of the rendered message for the names), so property names containing commas, whitespace, empty strings, or JSON-Pointer-escape-worthy characters survive correctly. The walker also descends throughitemsfor array-element segments, so cascades through{ data: [Item] }-shaped envelopes (single-schema items and Draft 07 tuple-form items, including the shapeOpenApiSchemaConverterlowers OAS 3.1prefixItemsto) collapse the same way. The walker recognises onlyproperties.<name>anditemstransitions and treats every other shape as unresolvable — composition keywords (oneOf/allOf/anyOf),additionalProperties: <schema>,patternProperties,additionalItems, and boolean schemas at item level all fall through to keeping the original message untouched, so a real additional-property violation is never silently swallowed.
- Query: only
style: form+explode: true(the OAS default). Specs declaringpipeDelimited,spaceDelimited,deepObject, orform+explode: falseare not parsed; type-mismatch errors will surface but they will point at the wrong cause. - Header / Path: only
style: simplefor scalar values.type: arrayandtype: objectparameters are not parsed (the raw string is fed to the schema, which then mismatches).style: matrixandstyle: labelfor path parameters are not handled — the prefix is not stripped before validation. - Cookie parameters (
apiKeysecurity scheme aside): not validated. parameters[].content: onlyparameters[].schemais read.
-
Validated:
apiKey(inheader/query/cookie) andhttp+bearer— presence checks for the named header/query/cookie / RFC 6750Bearertoken. -
Loud
E_USER_WARNINGon first encounter:oauth2,openIdConnect,mutualTLS, andhttpschemes other thanbearer(basic,digest). When every scheme in a security requirement is unsupported the requirement still passes (false-negative avoidance — blocking the test for a spec we cannot evaluate is worse than letting it through), but the validator fires a one-shot per-scheme-name warning so the silent pass does not stay invisible. The warning is emitted as a single line (shown wrapped here for readability):[security] OAuth2 scheme 'oauth2_user' is silently passed (no token check) — POST /v1/users. The opis/json-schema-based validator cannot verify oauth2 / openIdConnect / mutualTLS / http-basic / http-digest credentials. Your test will not detect a missing or invalid token. Workaround: split the bearer-token surface into a separate test, or assert the Authorization header presence manually.Under
phpunit.xmlfailOnWarning="true"this surfaces as a test failure on first encounter — the recommended setting if your spec contains any of these scheme types, since green tests against unauthenticated requests are the worst-class silent failure for a contract-testing tool.
- Validated (delegated to opis Draft 07):
type,enum,multipleOf,minimum/maximum/exclusiveMinimum/exclusiveMaximum,minLength/maxLength/pattern,minItems/maxItems/uniqueItems,minProperties/maxProperties/required,additionalProperties(true/false/ schema),allOf/oneOf/anyOf/not. format(validated by opis Draft 06+): the canonical 19-entry set (email,uuid,date,date-time,uri,ipv4,ipv6,hostname,regex,json-pointer, …). The full list is the authoritativeKNOWN_OPIS_FORMATSconstant insrc/Spec/OpenApiSchemaConverter.php— keeping it in one place avoids drift when opis adds formats. Unknown values (e.g.format: emialtypo foremail) emit a one-shotE_USER_WARNINGper format value, since opis silently accepts any value for unrecognised formats. Non-stringformatvalues fire a separate malformed-spec warning.- Advisory
format(deliberately not enforced, no warning):int32,int64,float,double,byte,binary,password. Treated as documentation hints per OAS conventions; seeADVISORY_FORMATSconstant. - Lowered:
const→enum: [value](3.1). - Stripped:
discriminator(includingmapping),xml,externalDocs,example/examples,deprecated, OAS-onlynullable/readOnly/writeOnlyafter enforcement (3.0), and Draft 2020-12 keys$dynamicRef/$dynamicAnchor/contentSchema(3.1). - Validated via opis (Draft 06+):
patternProperties,contentMediaType,contentEncoding. These are JSON Schema keywords that opis implements natively, so your constraints are enforced. - Not supported (loud E_USER_WARNING when first encountered):
unevaluatedProperties,unevaluatedItems. These are 2019-09 keywords with no Draft 07 equivalent — opis silently ignores them, so the warning surfaces specs that depend on them. Rewrite usingadditionalProperties: falseplus explicitpropertiesto enforce object closure. - Advisory-only (loud E_USER_WARNING when first encountered):
dependentSchemas,dependentRequired. These 2019-09 property-dependency keywords are not registered by opis Draft 07, so the constraint is dropped wholesale — a payload carrying the trigger property without its dependents passes silently. Rewrite as a Draft 07 conditional withif/then/else(theifclause tests for the trigger property, thethenclause carries the dependent requirement). discriminator: the keyword is dropped; the underlyingoneOf/anyOfis still validated as a union, butdiscriminator.mappingdoes not steer validation toward a single branch. Whenmappingis non-empty the converter emits a one-shotE_USER_WARNINGso polymorphic specs with serialiser bugs surface as a loud signal rather than silently passing through any valid branch.readOnly/writeOnly: enforced at the property's own top level only (see readOnly / writeOnly enforcement).
The PHPUnit coverage report counts GET, POST, PUT, PATCH, DELETE. Operations under HEAD, OPTIONS, and TRACE are not part of the coverage allowlist, and the Laravel auto-validation hook silently skips them (it normalises the request method through HttpMethod::tryFrom(), which returns null for these). Direct calls to OpenApiResponseValidator::validate() with one of these method strings will resolve against the spec — but if you depend on coverage tracking or the Laravel trait, treat HEAD / OPTIONS / TRACE as out of scope today.
Webhooks (3.1), Callbacks, Response Links, Server URL templating (servers with variables), Examples (examples blocks at parameter / requestBody / response level — not used for fuzzing or validation), tags, externalDocs, vendor extensions (x-* keys, ignored harmlessly).
The library uses PHP's native trigger_error(..., E_USER_WARNING) as the loud-signal channel for silent-pass conditions the validator cannot enforce. This is the v1.0 official API: warnings are dedup'd per-process and prefixed with a category tag so callers can route or filter them mechanically.
| Category prefix | Source | Dedup key |
|---|---|---|
[security] |
SecurityValidator (oauth2, openIdConnect, mutualTLS, http-basic, http-digest) |
scheme name |
[OpenAPI Schema] |
OpenApiSchemaConverter (unevaluatedProperties / unevaluatedItems, dependentSchemas / dependentRequired, discriminator.mapping, unknown / malformed format) |
per-keyword / per-format-value |
How to consume:
- Default (PHPUnit
failOnWarning="true"): the first warning fails the test. Recommended for contract-testing pipelines, since silent-pass on auth or unknown formats is the worst-class failure mode. - Stay green, surface warnings in output: omit
failOnWarning(PHPUnit 10+ default isfalse). Warnings show in the test report but do not fail. - Capture programmatically (e.g. for a custom report):
set_error_handler(static function (int $errno, string $errstr): bool { if ($errno === E_USER_WARNING && str_starts_with($errstr, '[security]')) { MyReport::record($errstr); return true; // suppress } return false; // bubble });
- Suppress one category (e.g. acknowledged limitation): match on the category prefix in your error handler. Do not blanket-suppress all
E_USER_WARNINGs — unrelated warnings would silently disappear.
Why not exceptions / PSR-3 logger / structured payload on OpenApiValidationResult? The simple channel is zero-dep, integrates with every PHP framework's existing error handler, and stays out of the v1.0 SemVer surface. A structured channel (WarningCollector, PSR-3 sink, or result->warnings()) can be added in v1.x as additive without breaking — we are deliberately deferring until real-world usage demands it. See issue #149 for the design discussion.
Per-process dedup vs per-test: the dedup state is process-global. PHPUnit runs all tests in one process by default, so a warning fired in test A is not fired again in test B even if both schemas exhibit the issue. The *::resetWarningStateForTesting() helpers (annotated @internal) exist as test seams for the converter / security validator's own tests; downstream tests rarely need them.