All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Canonical-correctness release: audit findings B1, M1-M6, m2, m3, m4 and m8.
- Parameter
explodenow defaults totruefor the effective styleform(query and cookie parameters without an explicitstyle); an explicitexplodein the spec always wins. Serialized documents may emitexplode: truewhere they previously omitted it. Opt out withOpenApiValidatorBuilder::withLegacyExplodeDefaults(). - A Parameter Object declaring both
contentandschemafails the build withInvalidSchemaException(every location andcomponents/parameters); exactly one of the two forms must be used. - In OpenAPI 3.0 documents the
requiredcheck no longer applies toreadOnlyproperties during request validation (OAS 3.0.3: "required will take effect on the response only"). Sending areadOnlyproperty in a request body is still rejected. Opt out withOpenApiValidatorBuilder::enforceRequiredForReadOnly(). - In OpenAPI 3.2 documents the discriminator is a hint: an unknown or invalid discriminator value
falls back to plain
oneOfvalidation instead of rejecting the payload, so the outcome follows the regularoneOflogic. OpenAPI 3.0/3.1 documents keep the decisive semantics. Opt out withOpenApiValidatorBuilder::withDecisiveDiscriminator(). - Canonical
$id/anchor diagnostics run at build time: a duplicate$id(different schema instances behind one URI), an$idcontaining a fragment, and an$anchor/$dynamicAnchorvalue that does not match^[A-Za-z_][-A-Za-z0-9._]*$fail the build withDuplicateSchemaIdException,SchemaIdFragmentExceptionorInvalidAnchorException(previously the registry silently kept the first occurrence and ignored anchor spellings).
OpenApiValidatorBuilder::withLegacyExplodeDefaults(),enforceRequiredForReadOnly()andwithDecisiveDiscriminator()opt-outs (see Breaking Changes 1, 3 and 4).validateCallback(ServerRequestInterface $request, string $callbackName, ?CallbackContext $context = null)and theValidator\Callback\CallbackContextDTO: callback runtime expressions —{$request.body#/{pointer}},{$method},{$url},{$request.path.{name}},{$request.query.{name}},{$request.header.{name}}— are evaluated against the triggering request when the context is supplied, and the resolved path is compared with the request path. Without the context the previous strict fail-closed/lax fallback rules apply unchanged; with the context an unresolvable expression raisesUnresolvableCallbackPathExceptioninstead of wildcarding, and$response.*/$statusCodeexpressions are unresolvable by definition for a callback.- Content parameters are validated for every location (
query,header,path,cookie): the value is parsed by the first declared media type — JSON media types (application/json,+jsonsuffixes) are decoded, malformed JSON raisesInvalidParameterException— and validated against the media type schema. Non-JSON media types keep the raw string value. int32/int64formats are additionally registered undertype: number(thetype: integerregistrations are preserved), sotype: number+format: int64asserts the 64-bit range.- Three build-time schema diagnostics (see Breaking Changes 5) plus forced resource-registry
construction: the registry is built during
build()instead of lazily on the first$refresolution, so the diagnostics surface there. PackageInfo::VERSIONexposes the package release.
- The parsed-document cache fingerprint includes the package version (
PackageInfo::VERSION): after upgrading, every cached document is regenerated automatically — a one-time cache warm-up with no user action; stale entries expire by TTL.
unevaluatedItems/unevaluatedPropertiesannotations are now written on the parent instance path: anallOfbranch carryingunevaluated*keywords marks the items/properties it evaluated for the parentunevaluated*check, so{allOf: [{unevaluatedItems: {type: integer}}], unevaluatedItems: false}validates[1, 2]instead of rejecting it.- Numeric formats follow the OAS data type format rules:
int32/int64validate integer-valued floats (3e9raises an int32 range error,2.0passes,1e19raises an int64 range error by comparing beyondPHP_INT_MAXas a float, and1.5raises "requires an integer value");float/doubleaccept integer values when the schema declarestype: number(integer5is a number);int32/int64are asserted undertype: numberas well.
uniqueItems,constandenumcompare numbers through the internal scalar key codec: integers above the IEEE-754 double safe-integer boundary (2^53) and their mathematically equal float spellings are not considered equal — see the note in the README's Array Validation section.
Consolidated release of the full OpenAPI 3.2 / JSON Schema 2020-12 implementation program. The detailed per-change entries for every feature below follow in the [0.9.0] section.
contentEncoding/contentMediaType/contentSchemaare annotation-only by default (validation §8.1): string contents are no longer decoded, parsed or validated unlessOpenApiValidatorBuilder::enableContentAssertions()is called.- An unknown
contentMediaTypeno longer throwsInvalidContentMediaTypeException: it is collected as an annotation and emits aValidationWarningEvent; in assertion mode it warns and skips instead of failing. contentEncodingassertions (opt-in) cover the full RFC 4648 family plus quoted-printable, and7bit/8bit/binaryare identity encodings; previously onlybase64was asserted and other encodings were skipped silently.contentSchemais now applied by the assertion pipeline (decode, media check, sub-schema validation) instead of being parsed and ignored.- Response body validation is fail-closed by default: an unmatched response media type raises
UnsupportedMediaTypeExceptioninstead of skipping validation.withLenientResponses()restores the previous behavior and dispatchesUnsupportedResponseMediaTypeWarningEventper skipped body. - Unknown schema keywords are rejected at build time by default (strict keywords) instead of being
silently dropped. Opt out with
disableStrictKeywords(), legalize individual keywords withwithAllowedUnknownKeywords(), or register a handler withwithKeywordValidator(); keyword values stay preserved inextrasin every mode. $dynamicRef/$dynamicAnchorare fully implemented (dynamic scope, outermost anchor, per-encounter resolution) instead of being silently ignored, so results can differ on schemas using them.$id,$anchor,$defs,$vocabularyand$commentare parsed and honoured instead of dropped;$vocabulary: truefor an unsupported vocabulary refuses the build withVocabularyNotSupportedException.- OpenAPI 3.0 documents validate by the 3.0 dialect: array
typeis refused (TypeArrayNotSupportedException), keywords beside$refare ignored, andnullable: truewithout an explicittypeno longer makesnullacceptable. - Security validation dispatches through a handler registry with built-in presence/format handlers for
http/basic,http/bearer,http/digest,apiKey,mutualTLS,oauth2andopenIdConnectinstead of throwingUnsupportedSecuritySchemeExceptionfor everything but bearer/apiKey. The exception remains for scheme types without any handler; register custom handlers withwithSecuritySchemeHandler(). - OAuth Flows serialize the device flow under the
deviceAuthorizationwire key (the pre-releasedeviceCodekey is no longer accepted),SecuritySchemegainsdeprecated,schemeis normalized to lowercase, andoauth2MetadataUrlis validated as an absolute URI at parse time. - Custom schema keyword validation is public API:
KeywordValidatorInterface+withKeywordValidator()replace the hardcoded validator list. - External
$refresolution is pluggable:withExternalRefResolver()injects a customExternalRefResolverInterface, andwithDocument()registers companion documents by retrieval URI; previously the builder only created the builtinfile://resolver and documents could not be supplied. - Unchanged by design: format keywords stay asserted by default (the package's HTTP-validator profile).
Explicit handle:
withFormatAssertionMode(FormatAssertionMode::Annotation)switches to the JSON Schema 2020-12 annotation default. - Unchanged by design:
http/httpsexternal$reftargets stay denied by the builtin resolver (SSRF). Explicit handle: supply the documents withwithDocument(), or inject a network resolver withwithExternalRefResolver().
- Epoch A — Reference and Resource Model: schema resource identification fields,
extrasround-trip for unknown keywords, the per-document resource registry, full$dynamicRefdynamic-scope semantics, the$vocabularyguard with refuse semantics, legacy$recursiveRef/$recursiveAnchorwarning events, and multi-document support (withDocument(),withExternalRefResolver()). - Epoch B — Schema Semantics: the
DialectProfile3.0/3.1 branching,dependentRequired(plus the legacy draft-07dependenciestranslation), the annotation-onlycontent*default with the full opt-in assertion pipeline (ContentValidationResult,contentResults()/contentResultAtPath()), the public keyword-validator registry, strict unknown keywords by default, and the complete format vocabulary with thewithFormatAssertionMode()switch. - Epoch C — HTTP Surface: fail-closed response content negotiation with the lenient opt-out,
media-type kind classification (complete vs streaming vs sequential vs binary), the multipart 3.2
surface (nested encoding, ordered unnamed parts, byte ranges, nested
multipart/mixed, streaming part-by-part validation,contentTypelist form), and XML Object projection for XML request bodies. - Epoch D — Security: the full 3.2 Security Scheme model, the
SecuritySchemeHandlerInterfaceregistry with seven built-in presence/format handlers,withSecuritySchemeHandler(), and the promotion ofSchemeCredentialReporterto the publicValidator\Securitynamespace.
- Domain exceptions of the spec parse phase surface raw from the builder instead of being wrapped
into
BuilderException:TypeArrayNotSupportedException(OpenAPI 3.0 arraytype),InvalidUtf8ExceptionandSpecTooLargeExceptionpass through uncaught, so callers can catch the domain type directly; every other parse failure keeps theBuilderExceptionwrapper with the original exception asprevious.
LinkContext::fromGroups()and the internalLink\Internal\RequestScope/ResponseScopeDTOs (unused static factory over internal types; theLinkContextconstructor remains the single supported instantiation path).
withMaxJsonBodySize()is now enforced on buffered request bodies (previously the cap was silently ignored on that path and only the decoded-content pipeline honoured it).- The discriminated
oneOfnull short-circuit honours the 3.0 dialect profile: an inline untypednullable: truemember no longer makesnullacceptable in OpenAPI 3.0 documents (3.1 semantics are unchanged), completing thenullableRequiresTyperule of change 9.
Note
Entries in this section include the full-spec-implementation work continued after the 0.9.0 release cut (at commit 14be707); see [0.10.0] for the consolidated release notes of that effort.
-
Vendored the canonical draft 2020-12 metaschema (
https://json-schema.org/draft/2020-12/schemaand its sevenmeta/*documents) byte-for-byte undertests/Fixture/JsonSchemaTestSuite/metaschema/with source commit pin, per-file SHA-256 checksums and the upstream license notice. The JSON Schema Test Suite runner registers these documents by their canonical retrieval URIs, so suite cases that reference the metaschema (for exampleref.json"remote ref, containing refs itself") now validate against the real 2020-12 metaschema instead of failing scheme allowlisting; product file-resolution allowlisting is unchanged. -
Streaming multipart request validation (3.2 §4.15.4.8): a
multipart/*request is now processed part by part straight from the PSR-7 stream instead of being read into a single buffer.RequestValidatorroutes multipart requests through the newRequestBodyValidatorInterface::validateStream()entry point (implementors of the interface must add the method); the internalStreamingMultipartDetectordecides the routing by media type (case-insensitive, matching content-type negotiation), and requests whose matched Media Type Object does not declareprefixEncoding/itemEncodingfall back to the previous buffered pipeline after a limit-checked read, somultipart/form-dataand named-part validation keep their exact former behavior. Ordered multipart bodies are split incrementally by the CRLF-anchored boundary framing of RFC 2046: a delimiter candidate must sit at a line start and be followed by--(close) or transport padding plus CRLF per RFC 2046 §5.1.1 — a close candidate followed by any other byte (for example a fake--boundary--garbagesequence inside binary part content) is treated as part content, not as the end of the body; a bare close delimiter without the trailing CRLF is still accepted at the true end of the stream, and boundary-token prefixes inside part content no longer create phantom parts. Each completed part is encoded-asserted, coerced (when coercion is enabled, per part viaRequestBodyCoercer+TypeGuarantor) and validated againstprefixItems[i] ?? itemSchema ?? schema.items— the same schema also drives the part value shaping and part-level example warnings — item-level results are available as parts arrive, while whole-array keywords (for exampleminItems) are not validated on the streaming ordered multipart path. Existing limits are reused: more than 1000 parts raiseValidator\Exception\MultipartBodyTooLargeException, a single part larger thanwithMaxMultipartBodySize()raisesValidator\Exception\BodyTooLargeException, and the nested-multipart recursion applies the sameMultipartNestingDepthExceptiondepth cap configured for the buffered resolver and applier. Known limitation: on the streaming ordered multipart path a whitespace-only body againstrequired: trueis not rejected (MissingRequestBodyExceptionstays a buffered-path behavior), because rejecting it would require buffering; an empty (zero-byte) stream is still rejected. -
Byte-range parts (3.2 §4.15.4.9, RFC 9110 §14.5/§14.6): a part
Content-Rangeheader declared inencoding.headersis parsed strictly into theByteRangevalue object (first/last/complete, nullable for*) byByteRangePartParser. The parser accepts exactlybytes <first>-<last>/<complete>,bytes <first>-<last>/*andbytes */<complete>; a missing/completepart, a missingbytesprefix, trailing garbage or a trailing line break, and a range whose last byte position precedes the first are rejected withValidator\Exception\InvalidContentRangeException(keywordformat). -
XML Object semantics for XML request bodies (3.2 §4.26): an XML body validated against a schema is now mapped through the XML Object model instead of generic name-based parsing. Element and attribute nodes are matched by the XML Object
name/namespace/prefixfields (namespace URI identity, per §4.26.4),nodeType(element/attribute/text/cdata/none) is honored together with the deprecatedattribute/wrappedboolean fields,nodeType: noneschemas inline their subschemas under the parent node, arrays map as unwrapped item sequences orwrapped/nodeType: elementwrappers, and namedprefixItemsmap positionally by node name. §4.26.5 null handling: an element carryingxsi:nil="true"(detected namespace-awarely againsthttp://www.w3.org/2001/XMLSchema-instance) maps tonull; an omitted attribute maps tonullwhen the property schema allowsnull(type includes"null") and the property is omitted otherwise; an absent element node always means an absent property (nullis never inferred from a missing node). The mapping is schema-driven projection only — it is not XML Schema validation (ADR, Epoch C boundary). Schemas whose shape cannot determine a mapping (free-form objects, unnamed array items) keep the generic pass-through shape; a schema carrying$ref/$dynamicRefdefaults tonodeType: none(§4.26.1), so a property whose$refstays unresolved contributes no property to the mapped value. A declared wrapper that does not match the document is reported as aValidationWarningEvent(carryingpropertyNameandpropertyPath) and the node passes through unmodified instead of failing. The mapping applies toapplication/xml/text/xmlrequest bodies (the media type schema is now passed intoBodyParser::parse()andXmlBodyParser::parse()as an optional parameter — existing signatures stay source-compatible) and to multipart XML leaves (MultipartInstanceResolverfeeds the part schema in). All simplexml operations keep running insideLibxmlSecuredContext, and the 1 MiB XML byte cap (DEFAULT_MAX_XML_BYTES) plusBodyTooLargeExceptionapply unchanged, including multipart leaves. -
Built-in security scheme handlers for HTTP Basic and HTTP Digest authentication (RFC 7617/7616):
http/basicandhttp/digestschemes are now dispatched to dedicated built-in handlers in theValidator\Securityregistry instead of raisingUnsupportedSecuritySchemeException. Both handlers stay at the presence-and-format depth of the bearer handler: theAuthorizationheader must carry theBasic/Digestprefix (case-insensitive) followed by a well-formed token (Basic, exactly one space and a base64/base64url token per RFC 7617) or a well-formedkey=valueparameter list (Digest, comma-separated with optional whitespace, values as RFC 7230 tokens or quoted strings with backslash escapes per RFC 7616). Credential values are never decoded, compared or reported: failures raise the generic missing-credentials error with scheme typehttp/basicorhttp/digestand locationAuthorization header, and the verbose log entry keeps the exact bearer handler shape (debug level, scheme name, scheme type and location only). The built-in registry order is basic, digest, bearer, api key, so a custom handler registered throughwith()still wins over every built-in one;http/bearerdispatch is unchanged. -
Built-in security scheme handlers for mutual TLS, OAuth2 and OpenID Connect security schemes (3.2 §4.27.5, §4.27.2, §4.27.3):
mutualTLS,oauth2andopenIdConnectschemes are now dispatched to dedicated built-in handlers in theValidator\Securityregistry instead of raisingUnsupportedSecuritySchemeException. The mTLS handler is a presence heuristic over request headers only (SSL_CLIENT_CERT,X-SSL-CERT,X-Client-Cert, case-insensitive): any of them present means success plus awarning-level log entry on every triggering validation (client-suppliedX-SSL-*headers are spoofable when a reverse proxy does not rewrite them — presence is trusted proxy configuration, never proof of authentication;getServerParams()is intentionally not consulted), and no candidate header means the generic missing-credentials error. The oauth2 and openIdConnect handlers are Bearer presence checks (case-insensitiveAuthorization: Bearer <token>pattern shared with thehttp/bearervalidator): a missing or malformed header raises the generic missing-credentials error with scheme typeoauth2/openIdConnect, and a valid one succeeds plus a debug log entry carrying the scheme's declared scopes — the union of thescopesmaps of all five OAuth flows (implicit,password,clientCredentials,authorizationCode,deviceAuthorization, first flow wins on key collisions) merged with the flat scheme-levelscopesmap when present. No token introspection, no network calls, and no token or certificate values in logs — the depth stays at presence and format. The oauth2 handler format-checksoauth2MetadataUrlwhen present and the openIdConnect handler format-checksopenIdConnectUrlwhen present (invalid absolute URI raises the generic missing-credentials error at request time; the URI predicate and message are shared with the parse-time validation inSecuritySchemeBuilder, which keeps raisingInvalidSchemaExceptionwhile parsing the document). The built-in registry order is basic, digest, bearer, api key, mutual TLS, oauth2, open ID connect, so a custom handler registered throughwith()still wins over every built-in one; thehttp/bearerfallback (bearer pattern applied to everyhttpscheme without its own handler) remains the dispatch path for futurehttpsubschemes. -
OpenApiValidatorBuilder::withSecuritySchemeHandler(SecuritySchemeHandlerInterface $handler): self(fluent, repeatable): registers a custom security scheme handler through the public builder surface, closing breaking#10 — user-defined security schemes (for example an introspection stub forhttp/beareror a custom apiKey location) are now implementable without touching package internals. The handler is prepended ahead of the current handler list inValidator\Security\SecuritySchemeHandlerRegistry, and the first handler whosesupports()returns true wins the scheme lookup, so a user handler overrides every built-in one for the schemes it claims (basic, digest, bearer, api key, mutual TLS, oauth2, openIdConnect); registering two competing handlers makes the lastwith()call win (LIFO order). The merged registry (user handlers first, built-in set behind them) is shared by request, webhook and callback security validation; the OR/AND security requirement semantics are unchanged. -
SchemeCredentialReporteris promoted fromValidator\Security\Internalto the publicValidator\Securitynamespace: the reporter type appears in the publicSecuritySchemeHandlerInterface::validate()signature, so external handler implementations must be able to name it. The class keeps its shape (publicschemeName,reportMissing(schemeType, location)), its caller-safeMissingSecurityCredentialsErroroutput and the sanitized verbose logging; the@internalmarker is removed.
-
Built-in string format validators now conform to the official draft 2020-12 format test suite: all twenty vendored
optional/format/*sections exceptecmascript-regex(PCRE/ECMA-262 engine boundary) are active and green; the fourteen sections activated by the conformance repair contribute 646 cases. Format validation becomes stricter and more correct — schemas asserting the formats below now reject inputs they previously accepted and accept inputs they previously rejected:hostnameis ASCII-only per RFC 1034/1123 (non-ASCII input — full-width label separators, the Kelvin sign, IDN separators — is no longer silently mapped through IDNA and is rejected);xn--A-labels are verified (Punycode decode, RFC 5892 code-point validity, canonical re-encode round-trip), so invalid, non-canonical or disallowed-content Punycode labels are rejected.idn-hostnameperforms full IDNA2008 validation: UTS#46 mapping and validity through ext-intl plus the rules ICU does not implement — RFC 5892 Appendix A CONTEXTO rules (MIDDLE DOT, GREEK KERAIA, HEBREW GERESH/GERSHAYIM, KATAKANA MIDDLE DOT, Arabic-Indic digit mixing), the RFC 5892 §2.6 DISALLOWED exceptions (Arabic tatweel, Nko lajanyalan, Hangul tone marks, vertical kana repeat marks, vertical ideographic iteration mark), and RFC 5891 §4.4 A-label verification (decode to a U-label, RFC 5892 code-point classes, canonical round-trip) with Bidi and join-control checks. When the ext-intl extension is absent the validator fails closed: non-ASCII input and A-labels are rejected instead of passing through unchecked. Installing ext-intl in production is recommended whenever schemas declareidn-hostname(orhostnamewithxn--A-labels) so full IDNA2008 validation is performed rather than the fail-closed fallback.uri,uri-reference,iriandiri-referencevalidate the full RFC 3986/3987 grammar per component: scheme, userinfo (at most one@), host (bracketed IP literals validated as addresses, including embedded IPv4 octets; unbracketed hosts must not contain colons), path/query/fragment character classes and percent-encoding triplets. One deliberate deviation from the RFC 3986 grammar (port = *DIGIT, which sets no range): a numeric port part must not exceed 65535 — this cap is a validator semantic bound, not part of the RFC grammar. Characters forbidden by the RFCs — space,",<,>,\,^,`,{,|,},]outside brackets, control characters and (foruri/uri-reference) any non-ASCII byte — are rejected.iri/iri-referenceaccept UCSCHAR and (in queries) IPRIVATE per RFC 3987.uri-templatevalidates the RFC 6570 grammar: literal character classes, expression structure (optional operator, non-empty var-list, no empty varspecs, no trailing commas),varnamecomposition (dots only between varchars, pct-encoded varchars allowed), the prefix modifier (one to four digits, positive, no leading zero), the explode modifier, and their non-combinability. An apostrophe in a literal is accepted per RFC 6570 §2.1 (RFC 3986 reserved characters).emailvalidates pure RFC 5321 §4.1.2/§4.1.3 mailboxes and is ASCII-only: SMTPUTF8 input is no longer accepted (it belongs toidn-email, RFC 6531). Domains no longer require a dotted TLD — single-label domains and all-numeric labels are valid per RFC 5321 — while quoted local parts keep supporting spaces,@and quoted pairs. Address literals validate strictly: four IPv4 octets ≤ 255 and case-insensitiveIPv6:tags with real IPv6 addresses.idn-emailimplements RFC 6531/6532 SMTPUTF8 instead of forwarding to the ASCII validator: UTF-8 local parts (dot-atom and quoted forms) without an NFC requirement — C1 controls and noncharacters included — and UTF-8 domain labels with Ldh constraints (no leading/trailing hyphen, no empty labels, no leading or trailing dot).timeanddate-timeaccept the full RFC 3339 numeric offset range (±00:00–±23:59; previously only±00:00–±14:00was accepted) and a leap second (:60) is valid whenever the local time normalized by its offset equals23:59:60UTC — for example15:59:60-08:00— instead of only literalZ/±00:00offsets at23:59:60. Offsets of±24:00and beyond remain invalid, and trailing line breaks are rejected.durationenforces RFC 3339 duration composition:Y/M/DandH/M/Scomponents must appear in order (P1Y2D,PT1H2SandP1YTare rejected;P1Y1M1DT1H1M1Sis valid) and weeks are standalone (P1W2Dis rejected).uuidvalidates the RFC 4122 syntactic pattern (8-4-4-4-12 hex digits): version and variant nibbles are no longer restricted, so hypothetical future versions (for example version nibblef) and non-RFC-4122 variant nibbles are accepted.relative-json-pointerfixes the JSON Pointer part grammar (RFC 6901): pointer parts with multiple reference tokens (0/foo/bar,120/foo/bar) validate again, and trailing line breaks after the integer prefix are rejected.
-
Suite runner: the fourteen
optional/format/*sections join the active sections (646 cases); theFS-40pending residue drops from 1094 to 448 cases (skipped suite cases drop from 1180 to 534). -
Core instance semantics for schema validation (draft 2020-12 conformance; JSON Schema Test Suite sections
items,ref,optional/refOfUnknownKeyword,unevaluatedItems,unevaluatedProperties— 318 cases — are now active and green):- JSON Pointer fragments now percent-decode before
~0/~1unescaping per RFC 3986/RFC 6901 (%25→%,%22→"), so definitions likepercent%fieldreferenced as#/$defs/percent%25fieldresolve;$ref: "#"inside a subschema resolves to the root of the enclosing schema resource. - Local pointer references (
#/...) that miss the OpenAPI envelope now resolve against the root of the enclosing schema resource (#/properties/foo,#/prefixItems/0) and against unknown-keyword values kept verbatim in schema extras (#/unknown-keyword,#/properties/foo/unknown-keyword) and against list entries of the 2020-12exampleskeyword (#/examples/0); references into documents registered viawithDocument()resolve their local pointers against that document's resource. Envelope-rooted pointers (#/components/...) keep their previous fast path and do not touch the resource registry. items/prefixItemsapply to JSON arrays only: JSON objects (including JavaScript-style pseudo-arrays such as{"0": ..., "length": 1}) no longer feed their keys into item indexes, andnullitems validate against subschemas that do not restricttype(2020-12 semantics; OAS 3.0nullableRequiresTypeprofiles keep their behavior).- The 2020-12
exampleskeyword accepts the JSON Schema list form alongside the OAS map form (parser, sibling merge, serializer round-trip and example collection). unevaluatedProperties/unevaluatedItemsnow track evaluated names and indexes per instance location: an inner property or item does not satisfy anunevaluatedkeyword at another location;unevaluatedProperties: true/unevaluatedItems: trueevaluate everything they cover so nestedallOf-scoped guards compose; a schema-typedunevaluated*target marks the properties/items it applies to as evaluated; and sibling applicator keywords of a$refschema (itsproperties) are applied as a separate sibling application whose annotations are invisible inside the$reftarget (a$refcreates a new evaluation scope), while non-evaluation sibling keywords (nullable, bounds,required) keep merging into the resolved target as before.
- JSON Pointer fragments now percent-decode before
-
Suite runner: the five sections above are activated (1180 suite cases skipped overall, 1094 of them held by
FS-40per the pool decomposition attribution). -
UnsupportedSecuritySchemeExceptionmessage wording: with the full built-in handler set (apiKey, http/basic, http/bearer, http/digest, mutualTLS, oauth2, openIdConnect) the message no longer claims that the package "validates apiKey and http/bearer only" — it now states the fact that no built-in handler exists for the scheme type/subscheme and points to the public registration entry point: a custom handler must be registered viaOpenApiValidatorBuilder::withSecuritySchemeHandler()to validate it. The exception is now raised only for types orhttpsubschemes without a built-in handler (for examplehttp/negotiateor an entirely unknown type); scheme name, scheme type andhttpSchemeproperties are unchanged. -
XML empty-element parse semantics (3.2 §4.26.5): an empty element without
xsi:nil— root (<empty/>), child (<nickname/>) or repeated (<item/><item/>) — now maps to an empty string''instead ofnullin generic XML parsing;nullis now reserved forxsi:nil="true"elements only (detected against the XML Schema-instance namespace URI regardless of the bound prefix, in both generic and schema-driven paths). In the schema-driven mapping an empty element is typed by its schema: when the element type allowsobjectorarrayit maps to an empty object/list ([]; for a union the composite branch wins over scalar branches), and to''for scalar types — the §4.26.6.8productNoNullsfixture (<product count="42"><description>Thing</description><related></related></product>→related: {}) now validates without errors. Whitespace-only leaf text stays untrimmed (<root> </root>→' '), matching child leaf values, whiletext/cdatanode mapping and mixed-content#textstay trimmed. Root arrays without named items now collect children across all namespaces, not only the default one. This is a deliberate breaking change of the previously pinnedempty element → nullbehavior: per §4.26.5/§4.26.6.8 an empty element is a value (''/{}), notnull— and only<related xsi:nil="true"/>denotesnull. -
Multi-document OpenAPI descriptions (3.2 §4.1.2):
OpenApiValidatorBuilder::withDocument(string $retrievalUri, string $content)registers user-supplied documents by intended retrieval URI. Content is size-capped with the same limit asFileExternalRefResolver(withExternalRefMaxBytes()andDEFAULT_MAX_REF_BYTESapply, re-checked against the final limit atbuild()) and is parsed in full at registration time; parse failures raiseDocumentParseExceptioncarrying the retrieval URI without exposing document content. External$refresolution order is now: document repository (exact match, then base-URI combination against the current document$self) →file://jail resolver →UnresolvableRefException. External$refvalues inside the main spec and inside registered documents must either be satisfied by the repository or requireexternalRefAllowedRoot; the confinement verdict cache key includes the repository fingerprint. -
OpenApiValidatorBuilder::withExternalRefResolver(ExternalRefResolverInterface $resolver): selfinjects a custom external$refresolver ahead of the builtinfile://jail resolver, closing the gap where the interface existed but the builder never accepted one. The injected resolver serves every scheme (for examplehttps://documents), while the builtin scheme allowlist, path jail and size caps stay in force whenever no custom resolver is registered; theUnresolvableRefExceptionconfinement message now points at this entry point. -
Schema resource identification fields (
$id,$anchor,$dynamicAnchor,$dynamicRef,$comment,$vocabulary,$defs) and unknown schema keywords are preserved through parse/serialize round-trip; they were previously silently dropped on load. Unknown keywords keep their values verbatim (vendor extensions, typos,$recursiveRefincluded), so re-serializing a schema reproduces the source document. -
DialectProfileDTO (additive public API): the OpenAPI 3.0/3.1 dialect profile is derived once per document from theopenapiversion (DialectProfile::fromOpenApiVersion(), withforOpenApi30()/forOpenApi31()factories) and carried through ref resolution, validation context (including branch forks) and the schema parser build path (RefResolver::dialectProfile()exposes it). Behavior is unchanged in this release: no code path consumes the profile flags yet, and the stateless validation path keeps the 3.1 default. -
dependentRequiredkeyword with full cascade support and the legacy draft-07dependencieskeyword translated at parse time (list form to dependentRequired, schema/boolean form to dependentSchemas, empty list as vacuum), registered in the known keyword set. -
Custom schema keyword validation (public part of the keyword-validator registry):
OpenApiValidatorBuilder::withKeywordValidator(string $keyword, KeywordValidatorInterface $validator)registers a validator for a user-defined schema keyword (calls accumulate; re-registering a keyword replaces its validator while keeping its dispatch slot — fully for stateless-dispatched keywords only, since built-initems/properties/oneOfare additionally dispatched by the context path and cannot be fully replaced). Keyword values are read from the schemaextrasproduced by the parser — custom keywords are never added to the schema model. Registered keywords dispatch after the whole built-in set (includingunevaluatedItems/unevaluatedProperties), so applicator-style custom keywords cannot place themselves ahead of them; a keyword without a registration stays an extras annotation and does not affect the validation outcome. -
Format assertion mode switch:
OpenApiValidatorBuilder::withFormatAssertionMode(FormatAssertionMode $mode)selects betweenFormatAssertionMode::Assertion(the default; format keywords are asserted, behavior unchanged) andFormatAssertionMode::Annotation(format keywords are only collected as validation annotations and never asserted, matching the JSON Schema 2020-12 annotation default). In both modes the appliedformatkeyword is recorded in the run annotations.strictFormatskeeps its semantics: an unknown format token raisesInvalidFormatExceptionin assertion mode when strict formats are enabled; annotation mode never raises. -
Nested encoding application for multipart request bodies (3.2 §4.15.2): Encoding Objects reached through the Media Type Object's
prefixEncoding/itemEncoding/encodingfields are now applied one level deep to nested parts. A nested part's resolved Encoding Object checks the part'sContent-TypeagainstcontentType(string or list form,type/*wildcards included, parameters and case ignored), enforces requiredheaderson the part (Content-Typeinsideencoding.headersis ignored per §4.15.1.1), and, when the part is itself a multipart body, wire-splits it once by its own boundary to apply the nested fields to the inner parts. Mismatches raiseValidator\Exception\PartContentTypeMismatchError(keywordprefixEncoding,itemEncodingorencoding) and a missing required part header raisesValidator\Exception\MissingPartHeaderError. Parts without aContent-Typeheader are not content-type asserted, deeper than one level of nesting is passed through untouched, and schema-level recursion into nested multipart values stays out of scope; the wire parser remains a boundary splitter with no knowledge of the Encoding model. -
Ordered (unnamed) multipart request bodies (3.2 §4.15.4.6, §4.15.4.7): a
multipart/*Media Type Object carryingprefixEncoding/itemEncodingtogether with an array schema (oritemSchema) is validated positionally — parts map toprefixItems/itemsand toprefixEncoding/itemEncodingby index,Content-Dispositionnames do not participate, an instance shorter thanprefixEncodingignores the surplus Encoding Objects, and part values enter the JSON data model (JSON-decoded when the part's effective content type is JSON, raw text otherwise) so the schema validator applies per-position assertions.encodingcombined withprefixEncoding/itemEncodingraisesValidator\Exception\InvalidEncodingConfigurationException, as does positional encoding without an array schema oritemSchema(§4.14.1, §4.14.5.2). Partheadersdeclared in Encoding Objects now carry full Header Object semantics: a referenced header ($refinto#/components/headers/...) is resolved from the document's components, a required header missing on its part raisesMissingPartHeaderError, and a present header value is validated against the header'sschemawith the same coercion rules used for response headers. Media type keys with parameters (for examplemultipart/related; type=text/html) now match requests by media type proper. The effective content type of a part follows the §4.15.1.1 default table (header, thenencoding.contentType, then the schema-type default) and is exposed for reuse by nested multipart handling. -
Nested
multipart/mixedrequest bodies (3.2 §4.15.4.10): a part whose effective content type ismultipart/*is now parsed recursively into a typed part tree, so nested parts enter the JSON data model one level deeper (nested array schemas mapprefixItems/itemsby position, nested object schemas mapContent-Dispositionnames to properties, and the §4.15.1.1 default content-type table applies to nested leaves the same way, including JSON decoding of JSON-typed leaves and XML parsing ofapplication/xml/text/xmlleaves). A declaredcontentTypewildcard (type/*,*/*) on a part without aContent-Typeheader JSON-decodes the part only when its schema position expects an object or array. Recursion is capped (one nested level below the outer body); a payload that nests deeper raisesValidator\Exception\MultipartNestingDepthExceptioninstead of recursing unbounded. Schema-less bodies keep the previous pass-through behavior and the wire parser remains a boundary splitter with no knowledge of the Encoding model.
-
Part headers declared in Encoding Objects with a content-based Header Object (
content.<media type>.schema) are now value-validated with the same coercion rules used forschema-based headers; previously such headers were checked for presence only. As a consequence, a value that fails the media type's schema (for example aformat: uriviolation onContent-Location) is rejected, and a declared partContent-Rangeheader is additionally parsed strictly per RFC 9110 (see Added). -
multipart/form-datarequests are wire-parsed once per validation: the encoding applier now receives the already-split parts instead of re-splitting the raw body, removing the duplicate multipart parse from the named-part pipeline. -
Every
multipart/*request now enters validation through the streaming entry point; requests without positional encodings behave exactly as before (the body is read within the existing body-size limits and validated by the unchanged buffered pipeline), while positional (ordered) multipart bodies are validated part by part without buffering the whole payload. -
multipart/form-datarequest bodies with an object schema declaringpropertiesnow populate the object from the parts'Content-Dispositionnames (3.2 §4.15.4.5, RFC7578 §4.3): a part named like a property fills that property, several parts sharing one name are collected into the property's array (a single part of an array-typed property becomes a one-item array), and duplicate names against a scalar property produce a list that fails the property's type check instead of silently dropping parts. Requests that previously failed validation with arequirederror while sending a matching named part now validate. Object schemas withoutpropertieskeep the previous raw part-list behavior, and non-form-data multipart subtypes are unaffected. -
EncodingObjectcontentTypenow accepts the OAS 3.2 §4.15.4.1 list form: the wire value may be a single media type string or an array of media type strings (for example["text/css", "image/*"]),Schema\Model\Encoding::$contentTypecarries the unionstring|list<string>|null, and serialization emits back the original wire shape, so round-tripping preserves string form and list form verbatim. An empty array, an array containing non-string members, or a non-string non-array value raisesSchema\Exception\InvalidContentTypeExceptionat parse time. Streaming consumers read the item parser media type from the first list element (ExternalMediaType::primary()) and use the full list only for media type matching, andExternalMediaType::isDefined()applies any-of semantics to the list. Code that type-assumesstringonEncoding::$contentTypemust handle the list form. -
Unknown schema keywords are rejected at build time by default (strict keywords): a keyword outside the legal set (the known JSON Schema 2020-12 and OAS base-vocabulary keywords, vendor
x-extensions, the legacy$recursiveRef/$recursiveAnchorkeys, keywords registered throughwithKeywordValidator(), and the explicit allow-list) raisesInvalidSchemaExceptionnaming the keyword while the document is parsed, so typos such asrequirdfail fast instead of silently degrading to unused extras.disableStrictKeywords()restores the previous annotation mode (unknown keywords stay extras and produce annotations), andwithAllowedUnknownKeywords(string ...$keywords)legalizes individual keywords point-wise; both compose with the rest of the builder. The check is a single traversal of the already-parsed document, applied to the main spec only (documents registered viawithDocument()are not guarded). -
Response bodies are now validated fail-closed against the declared Content-Type: a response media type matching none of the operation's declared response media types (exact,
type/*or*/*) raisesUnsupportedMediaTypeExceptionon both the buffered and the streaming validation path instead of silently skipping body validation. The exception message lists only the declared media types and never echoes the offending Content-Type, matching the request-path contract.OpenApiValidatorBuilder::withLenientResponses()restores the previous permissive behavior and makes every skip observable: each skipped response body dispatches anUnsupportedResponseMediaTypeWarningEventevent (PSR-14) carrying the unmatched media type. -
$dynamicRefis now evaluated with the full JSON Schema 2020-12 dynamic reference semantics (core §8.2.3.2, Appendix C) instead of being ignored. Resolution happens at every encounter during validation, never at load time: a plain-name fragment bookended by a matching$dynamicAnchorin the same schema resource is replaced by the outermost matching$dynamicAnchorin the dynamic scope; otherwise the keyword behaves like$ref(with$anchorfallback). Schemas with$dynamicReftherefore validate by the recursive semantics, so results can differ from the previous silently-ignoring build. The dynamic scope tracks every$id-identified schema resource crossed by validation, including$refchain resources, and cyclic dynamic references terminate instead of overflowing the stack: re-entering the same dynamic reference target at the same data path within one run is skipped as already evaluated, while depth-growing recursion (including branch forks that reset the per-branch frame, e.g. not/oneOf/if-then-else) is stopped by the depth limit (SchemaDepthExceededException, MAX_DEPTH=64). Verdicts on spec-undefined infinitely recursive schemas are implementation-defined and pinned by unit tests.#resolves to the root resource of its document, and a bare-URI reference to a registered document whose root carries a different$idresolves to that registered root. -
$refchains now apply the sibling keywords of every intermediate chain resource at each hop instead of dropping them; entry resolution returns the entry schema with its$refintact and the chain is followed at validation time. -
RefResolverInterfacegainedresolveSchemaEntry(),resolveSchemaWithOverrideAndChain(),scopeBaseUri(),findDynamicAnchorInScope(),registeredDocuments()and an optional$baseUriparameter onresolveSchemaWithOverride(). External implementations of the interface must add these members to stay compatible. -
$reffragments are resolved by their JSON Schema 2020-12 kind. A fragment starting with/is treated as a JSON Pointer (RFC 6901~0/~1decoding stays localized to pointer segments); any other fragment is a plain-name fragment resolved through the$anchor/$dynamicAnchorindexes only, with NCName validation and without tilde decoding. Pointer fragments rooted at$defs, URI references whose resource matches an in-document$id, and bare plain-name fragments resolve through the document resource registry first and fall back to raw document navigation afterwards;UnresolvableRefExceptionis raised only after both are exhausted. Specs containing bare-fragment$refvalues (for example#myAnchor) no longer requireexternalRefAllowedRoot, because a bare fragment never leaves the document. Relative URI references resolve against the$idof the schema carrying them, then against the document$self. -
Validator\Error\ValidationContextandValidator\Validation\ValidationStackare now named-constructor-only services: their constructors are private and the only creation entry points areValidationContext::fromInit()andValidationStack::fromGroup(). Schema model classes keep public constructors. -
Documents declaring
$vocabulary(core §8.1.2) are now checked once per document at build time. A vocabulary URI outside the supported set (2020-12core,applicator,unevaluated,validation,meta-data,format-annotation,format-assertion,content, and the OAS 3.1 base vocabulary) with the valuetruerefuses the build withSchema\Exception\VocabularyNotSupportedExceptioncarrying the unsupported URI (the same document previously assembled without a diagnostic). The same URI with the valuefalsebuilds and dispatches anEvent\VocabularyDisabledWarningEvent; schemas keeping legacy$recursiveRef/$recursiveAnchorkeywords in extras build and dispatch anEvent\LegacyKeywordWarningEventpointing at the$dynamicRef/$dynamicAnchorreplacements. URI comparison is byte-wise, so differently cased or normalized URIs are treated as unsupported. RegisteredwithDocument()resources stay outside the guard: cross-draft resources may legitimately declare another dialect's vocabularies. -
OpenAPI 3.0 documents now refuse schemas whose
typekeyword is an array (OAS 3.0.3 §4.7.24.1 requires a string):build()raisesSchema\Exception\TypeArrayNotSupportedExceptionwhere the same document previously assembled with 3.1 union semantics. The message carries no document content. OpenAPI 3.1+ documents keep acceptingtypearrays, including the'null'member, andnullablefolding is unchanged. -
OpenAPI 3.0 documents now ignore schema keywords sitting next to
$ref(JSON Reference draft-03 §3: any members other than$refSHALL be ignored), at the referenced-property entry point and on every intermediate$refchain carrier alike: the resolved target is validated as-is instead of being merged with the stub. A 3.0nullable: truesibling of$reftherefore no longer widens the target, sonullvalues for such properties are rejected where they previously passed. OpenAPI 3.1+ documents keep applying$refsiblings per JSON Schema 2020-12 §8.2.3.1, including per-hop merging along$refchains. -
OpenAPI 3.0 documents now treat
nullable: trueas an extension of an explicittypein the same Schema Object (OAS 3.0.3 §4.7.24.2): anullableSchema Object without atypeno longer makesnullacceptable, sonullvalues for such properties are rejected where they previously passed.nullable: truebeside an explicittypekeeps acceptingnull, deferred decisions ($ref,allOf,anyOf,oneOfbranches) keep deferring, and anotsub-schema is now matched against anullinstance by the sub-schema's own keywords instead of rejecting the match up front — sonot: {}(the booleanfalseschema model) rejectsnullas the suite requires. OpenAPI 3.1+ documents are unchanged, includingtypeunions with the'null'member and parser-sidenullablefolding. The dialect decision is carried by the document'sDialectProfile, and the root-schema validator cache entry created while resolving$dynamicRefwithout a validation context now derives the profile from the document version instead of silently defaulting to 3.1. -
contentEncodingandcontentMediaTypeare annotation-only by default, matching JSON Schema validation §8.1: string instance contents are no longer decoded, parsed or validated, the keyword values are collected into validation annotations (contentEncoding/contentMediaTypekeys) instead, and an unrecognizedcontentMediaTypeemits a PSR-14ValidationWarningEventplus a log warning instead of throwing. Previously an invalidbase64value or an unrecognized media type failed validation. CallOpenApiValidatorBuilder::enableContentAssertions()to restore the previous assertion behavior (strict base64 round-trip check and media type syntax validation) as a transitional mode until the full content assertion pipeline ships. -
With
enableContentAssertions(),contentEncodingassertions now cover the full RFC 4648 family —base16,base32,base32hex,base64,base64url(padding optional, canonical pad counts enforced, mixed padding rejected) — plus RFC 2045 §6.7quoted-printablesyntax validation (uppercase=HHescapes,=CRLFsoft breaks, tolerated trailing whitespace, CRLF-only line breaks).7bit,8bitandbinaryremain identity encodings without data checks, and an unrecognized encoding token now emits a PSR-14ValidationWarningEventplus a log warning instead of being ignored silently. The strict canonicalbase64round-trip assertion is unchanged. -
With
enableContentAssertions(), content assertions now run the full pipeline: the string instance is decoded according tocontentEncoding(base64andbase64url), the decoded content is checked againstcontentMediaType(application/jsonis parsed, the other recognized media types keep their syntax checks), and a declaredcontentSchemavalidates the decoded value through the regular schema validator, so sub-schema violations reject the main validation. PreviouslycontentSchemawas ignored and only encoding/media syntax was asserted. Decoded content exceeding the configuredmaxJsonBodyBytescap raisesDecodedContentTooLargeExceptionbefore parsing — the cap applies to every media type, not only JSON — and unknown media types keep emitting the warning event. The pipeline outcome is exposed as aContentValidationResult(decoded value, media type validity, collected sub-schema errors) throughValidationContext::contentResult(), and the pipeline accepts a skip-media flag for callers that learn the media type from the message (content map/Encoding Object) instead of the schema. Sub-schema validation runs only whencontentMediaTypeis declared: per JSON Schema 2020-12 thecontentSchemakeyword is bound tocontentMediaType, socontentSchemawithout a media type stays an annotation in assertions mode as well. -
Every content instance of a validation run is now addressable instead of first-wins-only:
ValidationContext::contentResults()returns all content pipeline results of the run keyed by the data path of their string instance (/0,/props/payload, …) in attach/merge order,ValidationContext::contentResultAtPath()reads a single instance result by path, andcontentResult()keeps returning the first instance in traversal order. Results merged from successful compositional branches (oneOf/anyOf/if-then-else/discriminator) join the list in attach/merge order, results of failed branches never enter it, and when several results reach the same data path the result of the first attach is kept. Deferred results still compute lazily on the first read after the run and stay memoized. -
Without content assertions (annotation mode) the pipeline never runs during validation:
contentEncodingandcontentMediaTyperemain annotations, the outcome is untouched, and the sameContentValidationResultis attached to the validation context lazily — it computes on the first consumer read, so undecoded payloads stay untouched unless the result is inspected. -
The official JSON Schema test suite section
content.jsonis re-bound to the active sections: it was previously parked pending the content assertion pipeline work; all 18 cases assert annotation-only content semantics and pass on the current validator, following the rebind precedent of the earlier suite triage. -
Formats on schemas without an explicit
typeare no longer ignored: the format is resolved against the instance domain (string/number/integer) and asserted for matching instances in assertion mode. -
The format sections of the official JSON Schema test suite are re-classified after the format assertion mode work: the format-assertion, date, ipv4, ipv6, json-pointer, regex and unknown-format sections (242 cases) join the active sections and pass in the default assertion profile;
optional/format/ecmascript-regex.jsonjoins the excluded sections (PCRE is not ECMA-262);format.jsonand the remainingoptional/format/*sections stay pending on the suite residue backlog until the builtin format validators reach full suite conformance and the runner gains per-section validator profiles.format.jsondeclares the annotation-default profile and is fully green underwithFormatAssertionMode(Annotation)— a dedicated replay test pins all 133 cases. The suite runner now also treats the rawInvalidFormatException(the package's fail-fast format error convention) as an invalid verdict. -
Security Scheme Objects now model the OAS 3.2 security surface (§4.27–4.29). BC break for direct integrators. The OAuth Flows Object wire key
deviceCodeis renamed todeviceAuthorization(§4.28.1), so the device flow no longer disappears when a 3.2 document is parsed; documents still using the pre-releasedeviceCodespelling build with the flow silently absent — the old key is not accepted (no deprecation cycle applies).SecuritySchemegains thedeprecatedboolean field (§4.27): it is parsed and serialized, so adeprecated: truescheme survives round-trip instead of being dropped, and the flow-leveldeprecatedfield of OAuth Flow Objects keeps its separate meaning. Theschemefield is normalized to lowercase at parse time (RFC 9110 §11.1 case-insensitivity):Basicandbasicproduce identical models, and serialization emits the normalized value.oauth2MetadataUrlis now validated as an absolute URI at parse time; a malformed value raisesSchema\Exception\InvalidSchemaExceptionwithout echoing the offending value. -
Security scheme dispatch is now extensible through a handler registry instead of the hardcoded
http/apiKeymatch chain insideSecurityValidator. BC break for direct integrators. TheSecurityValidatorconstructor drops its unusedApiKeyValidatorparameter; handlers are resolved through the newValidator\Security\SecuritySchemeHandlerRegistry(built with the built-in bearer and api key handlers in one place), and custom scheme support is declared by implementing the newValidator\Security\SecuritySchemeHandlerInterface(supports()+validate()), with the registry picking the first handler whosesupports()returns true.with()prepends a handler ahead of the built-in set so a user handler overrides a built-in one. Request-visible behavior is unchanged: the OR/AND requirement semantics, theUnsupportedSecuritySchemeExceptionfor scheme types without a handler, and thehttpSchemeparameter reported for non-bearerhttpsubschemes all stay exactly as before;oauth2,openIdConnectandmutualTLSkeep raising the unsupported-scheme error until dedicated built-in handlers land (http/basicandhttp/digestgain theirs in this release, see Added above).
- Server-Sent Events records now concatenate a value-only
datafield onto a preceding emptydataline per the WHATWG event-stream algorithm: an event stream ofdata:followed bydata: helloyields the event data"\nhello"(leading newline preserved) instead of replacing the accumulated buffer with"hello". Concatenation of non-emptydatalines and the empty-string result of a lonedata:line are unchanged.
Schema\Model\Schema::withOverrides()is removed. Build a schema from constraint groups withSchema::fromConstraintGroups()(static factory) and apply partial updates withSchema::withOverrideGroups(), which takes the four group DTOsSchema\Model\Internal\ScalarFields,ArrayFields,ObjectFieldsandCompositionFieldsand replaces only the fields that are non-null in the supplied groups (nullableis a separate parameter, so it can be flipped back tofalse). TheSchemaconstructor is no longer deprecated: instantiating the model directly with named arguments is the canonical API.Validator\Error\ValidationContext::create()and the publicValidationContextconstructor are removed. Create contexts exclusively through the named constructorValidationContext::fromInit(Validator\Error\Internal\ValidationContextInit); the DTO carriespool,errorFormatter,nullableAsType,emptyArrayStrategyandmode, and the context always owns a freshBreadcrumbManager, so per-request isolation is guaranteed by construction. Branch contexts keep usingforkForBranch().- The public
Validator\Validation\ValidationStackconstructor is removed. Build a stack throughValidationStack::fromGroup()with aValidator\Validation\Internal\ValidationGroup(root:RootServicesDTO,options:ValidatorOptionsDTO, optionalbodyLimits). Validator\Exception\ValidationErrorInterface::getType()and all of its implementations are removed. Usekeyword(), which returns the same validation keyword ('type','minLength','format', ...).Builder\OpenApiValidatorInterface::getFormattedErrors()is removed. Format aValidationExceptionthroughValidator\Error\Formatter\ErrorFormatterInterface::formatException(). The output is byte-identical to the removed method when the validator was built with the default formatter; otherwise call the configured formatter'sformatException()directly.Builder\OpenApiValidatorBuilder::enableStrictCallbackRuntimeTemplate()is removed. Strict callback runtime template resolution is the default; usedisableStrictCallbackRuntimeTemplate()to opt out (see the security warning in the README).Validator\Exception\InvalidMultipleOfSchemaException::forLargeIntegerWithoutBcmath()is removed.multipleOfchecks fall back to pure-PHP decimal string modulus when the BCMath extension is unavailable, so the exception is never thrown;forNonPositiveValue()remains.
StreamingContentParser::parserFor()resolves the format parser responsible for a content type through the same detection logic asStreamingMediaTypeDetector::detect(), and the newStreamingFormatenum (NdJson,Sse,JsonSeq) is the single source of the routing decision. Detection semantics are unified onstr_containswith the full patterns (application/jsonl,application/x-ndjson,text/event-stream,application/json-seq) — the broader of the two semantics previously split between the dispatcher and the detector — and the boundary is pinned by detection vectors, including the explicit rejection of vendor-suffixed types such asapplication/vnd.foo+json-seq.- New domain exceptions
Validator\Exception\UnknownLinkException(extendsInvalidArgumentException) andValidator\Exception\MultipartBodyTooLargeException(extendsRuntimeException) replace bare SPL throws for undefined component links and the multipart part-cap violation.
- Streaming parsers now share one line/chunk reader, one record collector
and one media-type detector.
NdJsonParser::parseStream()andJsonSeqParser::parseStream()delegate toStreamLineReader(readLines()/readChunks()) instead of re-implementing the chunk-buffer loop, and the by-referencereadChunk(&$bomStripped)helpers are gone: the reader is stateless and the UTF-8 BOM flag lives inside each read pass, so BOM stripping works per pass no matter how many sequential streams are parsed through oneStreamingContentParser. Record counting, the 100 000-record cap, the JSON depth limit and the malformed-record policy (strict throw vs log-and-skip) moved into a new internalRecordCollector, keeping each format's existing degradation behaviour (NDJSON/JSON-seq appendnull, SSE keeps the raw value). Exceeding a line or record length limit now throws the domainStreamRecordTooLargeException(extendsRuntimeException, message unchanged) instead of a bareRuntimeException, and scalar JSON values (42,"text",true) remain collected as records per the JSON Lines and RFC 7464 specs. Limit values are unchanged. - String-loaded specs (
fromYamlString()/fromJsonString()) no longer pay a second, cache-bypassing parse on everybuild()for the external-$refconfinement guard. The clean confinement verdict is now cached under the document cache key with a_confinementsuffix (SHA-256 of the spec content plus the size/depth limits fingerprint), sharing the pool and TTL of the cached document, so hot-path builds perform no spec re-parse. A verdict that finds an external$refis never cached — theBuilderExceptionreproduces on every build. Specs loaded from files and configurations withoutwithCache()are unaffected. (#68) - All internal LRU caches now share one generic internal class,
Validator\Internal\LruCache(@template K of string,@template V):ValidatorPool,PathRegexCacheandRegexValidatordelegate to it instead of each maintaining their own cache/order/touch/evict arrays. Eviction order, touch semantics and the per-class default capacities (128 / 256 / 512) are unchanged, and the shared class owns theMax size must be at least 1constructor guard and a capacity-preserving no-optouch()for keys that are not stored. The TTL default and its validation now live in a single place:TypedCacheDecorator::DEFAULT_TTL(3600) withTypedCacheDecorator::validateTtl(), whichSchemaCacheuses for its constructor default and validation. - The optional
DeprecationLoggerconstructor parameterenabledis renamed toisEnabled(boolean-naming convention). Callers passing the flag by name (new DeprecationLogger(enabled: false)) must switch toisEnabled:; positional calls and the parameter's position and default (true) are unchanged. ValidationFinishedEventproperty names follow the naming conventions:successis renamed toisSuccessandduration(seconds) todurationInSeconds. Readers of the public readonly properties ($event->success,$event->duration) must switch to the new names; constructor argument names change with them. Types, defaults and dispatch semantics are unchanged.- The two
contentMediaType/contentEncodingvalidation-error classes move into the sharedValidator\Exceptionnamespace:Duyler\OpenApi\Validator\SchemaValidator\InvalidContentMediaTypeExceptionand...\InvalidContentEncodingExceptionare nowDuyler\OpenApi\Validator\Exception\InvalidContentMediaTypeExceptionand...\InvalidContentEncodingException. Catch-sites referencing the old FQCNs must update the import; class names, hierarchy (AbstractValidationError) and messages are unchanged. OpenApiValidator::resolveLink()/resolveLinkWithContext()throw the newUnknownLinkExceptioninstead of a bareInvalidArgumentExceptionfor undefined component links. The new class extendsInvalidArgumentExceptionand keeps the exact message (Unknown link: %s), so existing catch blocks continue to work.- Exceeding the multipart part cap throws the domain
MultipartBodyTooLargeException(extendsRuntimeException, message unchanged) instead of a bareRuntimeException. - The bearer Authorization check inside security validation now runs on the
same
PregExecutorinstance as every other regex in the validator: themaxRegexBacktrackslimit configured throughOpenApiValidatorBuilder::withMaxRegexBacktracks()governs thehttp/bearerscheme match too, instead of it silently using a second default-capped executor. TheSecurityValidatorconstructor no longer accepts abearerValidatorargument; the bearer validator is wired to the injected (or default)PregExecutor.
uniqueItemsand scalarenumno longer produce false duplicates and false acceptances around the IEEE-754 double representation. Both keyword paths now encode scalar comparison keys through one internalValidator\SchemaValidator\Internal\ScalarKeyCodecwhose float keys are an injective per-double-value hex encoding of the big-endian IEEE-754 bytes (pack('E', ...)), fixing the string-cast collapse where the old(string)-based keys were truncated to theprecisionini (14 digits by default — a different ini fromserialize_precision), so, for example,enum: [9007199254740990.0]incorrectly accepted9007199254740991.0anduniqueItems: truerejected[9007199254740990.0, 9007199254740991.0]as duplicates.-0.0and0.0now encode to the same key, so the two zeros are equivalent per JSON Schema 2020-12 §4.2.2.NaNvalues remain never-equal to anything (each NaN occurrence gets a unique key), and mixed int/float comparisons above the 2^53 safe-integer boundary stay rejected.- OpenAPI 3.0
nullable: trueis no longer discarded when a schema is reached through$ref.ScalarSiblingMerger::merge()combined the$refstub'snullableflag with the resolved target's using a logical AND, and becauseSchema::$nullabledefaults tofalsea bare{$ref: ...}stub always evaluatedfalse && true— erasing the target's nullability, so a legitimatenullwas rejected byTypeValidator. The flag now merges with OR: per OpenAPI 3.0 anullablesibling next to$refcan only widen the target, and there is no spelling for "narrow this to non-nullable" (#64). - A non-string type nested inside a parameter schema is no longer
unsatisfiable.
TypeCoercercoerced only the top-level schema type, soobjectandarrayparameters handed their un-coerced members to the schema validator:?page[limit]=3againstpage: {type: object, properties: {limit: {type: integer}}}could not pass for any value a client could send, since query, path, header and cookie values arrive as strings and coercion is the only path to a non-string leaf. Type dispatch is nowSchema-oriented and recurses throughpropertiesanditems, so?page[limit]=3,?filter[enabled]=trueand?ids=1,2,3coerce to['limit' => 3],['enabled' => true]and[1, 2, 3]. The traversal is shared withRequestBodyCoercer— which already recursed for request bodies — throughAbstractCoercer::coerceDeclaredProperties()andAbstractCoercer::coerceDeclaredItems()rather than duplicated. (#60) - Path Item level
parametersare now enforced during request validation.PathItem.parameterswere parsed into the schema model but never reachedRequestValidator, sorequiredandschemaconstraints on parameters hoisted to the path item — the place the specification encourages for parameters shared by every operation under a path — were silently ignored for every location (path,query,header,cookie). Path item parameters are now merged into the operation's parameter set when the operation is resolved, with an operation level parameter overriding the path level one on matchingname+in. The same merge is applied to webhooks and callbacks, which are Path Item objects as well. (#61) - An array-typed
formparameter no longer rejects a single value.ParameterDeserializer::deserializeForm()decided whether a non-exploded value was an array by looking for a comma instead of at the declared schema type, so?include=author,commentsdeserialized to a list while?include=authorstayed a string and failed thetype: arraycheck withTypeMismatchError. Form now routes on the schema type throughsplitBySeparator()— assimple,matrix,labelandcookiealready did — yielding a one item list for a lone value and an empty list for?include=. The comma heuristic remains for parameters that declare no array type, andexplodehandling is unchanged. (#58) - A
requestBodythat is a Reference Object is now resolved instead of silently discarded, so the referenced body is actually validated. The parser dropped$ref— it read onlydescription,contentandrequired— which produced an emptyRequestBody, andRequestBodyValidatorWithContextthen returned at itsnull === $requestBody->contentearly exit. Every request body behind a$refwas therefore unvalidated: malformed payloads passed andrequired: truewas not enforced, with no exception or warning. Reference support is now wired through all five layers thatParameterandResponsealready used —RequestBodygainsref/refSummary/refDescription,ComponentTreeBuilder::buildRequestBody()branches on$ref,DocumentNavigatoraccepts#/components/requestBodies/*targets, andRefResolverInterfacegainsresolveRequestBody()andresolveRequestBodyWithOverride(). Chained and dangling references behave as they do for responses: chains follow through, and an unresolvable or wrongly-typed pointer throwsUnresolvableRefExceptionrather than failing open. Webhooks and callbacks are covered too, since both validate throughRequestValidator. Per OAS 3.1+, adescriptionsibling of$refoverrides the referenced description (#56). anyOf/oneOf/allOfevaluate every branch again once the composition error cap is reached. A merge had restored the earlyreturninvalidateSchemas(), so a matching branch declared after the cap was never counted. Only error collection stops at the cap;allOfnow reports the true number of failed branches (#54)
- BC break for direct integrators. The experimental
ValidatorCompilersubsystem is removed —src/Compiler/(ValidatorCompiler,CompilationCache,CompilationCacheInterface, internal code generators and their exceptions),tests/Unit/Compiler/and the compiled-validator benchmark. Runtime validation,SchemaCacheand the spec-loading cache are unaffected. The package is pre-release; no deprecation cycle applies. - BC break for direct integrators. The
ConfinementVerdictsingleton class is replaced by the enumConfinementVerdict { case Clean }: type hints throughConfinementVerdict::class,instanceofchecks and stored cache values keep working, but code callingConfinementVerdict::clean()must switch toConfinementVerdict::Clean. No consumer inside the monorepo referenced the removed symbol (grep = 0). - BC break for direct integrators. The deprecated schema-validation stack
(
@deprecated since 0.6.0) is deleted:SchemaValidator\SchemaValidator,SchemaValidator\ValidatorDependencies(the legacy constructor-dependency bag),SchemaValidator\ValidatorFactory,Registry\DefaultValidatorRegistry,Registry\ValidatorRegistryInterfaceandException\UnknownValidatorException. Custom validator/registry injection through these classes is removed with no replacement; the 29 builtin validators are the complete set. All five internal consumption points had already migrated toSchemaValidatorWithContext/StatelessValidatorDispatcher/ theSchemaValidatorDependenciesDTO; nothing insrc/referenced the legacy classes. Integrate throughOpenApiValidatorBuilderor construct validators againstSchemaValidatorDependencies— validators now take that DTO in their constructors instead of the removed bag. The one shared keyword-applicability filter now lives inSchema\Internal\ApplicableValidators, and the singleitems: true|falseimplementation lives inSchemaValidator\Internal\BooleanItemsValidator. Validation\ValidatorDependenciesis renamed toValidation\ValidationStack(andValidation\Internal\ValidatorDependenciesGrouptoValidation\Internal\ValidationGroup) — one shortValidatorDependenciesname remains, theDto\ValidatorDependenciesservice container. The class was never README-documented; the only caller isOpenApiValidatorBuilder, which is updated. The 21-parameter constructor keeps its PHPDoc-only@deprecated(1.x window,fromGroup()replacement) — no#[Deprecated]attribute, so no runtimeE_DEPRECATEDcascade.- The optional
regexValidator/pregExecutorconstructor parameters ofSchema\StatelessValidatorRegistryare removed — the canonical defaults live onSchemaValidatorDependencies. With custom instances injected throughValidationStack/Builder(maxRegexBacktracks, sharedPregExecutor), the stateless registry's schema-level validators now use the default instances; request/response/context paths keep the injected ones. - The optional
?PregExecutorconstructor parameters ofResponseValidatorWithContext,ResponseBodyValidatorWithContextandRequestBodyValidatorWithContextare removed: header and body validation now resolve their regex executor from a single source,SchemaValidatorDependencies::pregExecutor, so direct construction can no longer mix two instances.
anyOf/oneOfno longer depend on branch declaration order. TheMAX_COMPOSITION_ERRORScap inAbstractCompositionalValidatorusedreturnto stop collecting errors, which also abandoned the remaining branches — so a branch that would match went unevaluated whenever earlier branches produced 20+ errors, andanyOfreported "At least one of the schemas must match, but none did". The cap now bounds error collection only; every branch is still evaluated. Error output is unchanged (20 errors plus oneTooManyErrorsError). (#54)
nullable: trueis now honoured when it sits beside a composition keyword instead of only on the branch schemas, so the standard OAS 3.0 workaround for a nullable$ref—{allOf: [$ref], nullable: true}— acceptsnull.AllOfValidator,AnyOfValidator,OneOfValidator,OneOfValidatorWithContextandIfThenElseValidatorshort-circuit when the schema carrying the keyword is nullable, rather than dispatchingnullinto branches that reject it.anyOf/oneOftreat this as the keyword being satisfied, not as a matching branch, sooneOfstill enforces exactly-one for non-null data. Anullmember of an OAS 3.1typeunion is ordinary JSON Schema and keeps composing — onlynullable: truewaives branches, and only whilenullableAsTypeis enabled (#50).
- A
nullpermitted by a branch of anallOf/anyOf/oneOfis no longer rejected before any branch runs. The null pre-check that guards property and item validation computed$allowNullfrom the immediate schema node, but a composition node carries notypeand nonullable— both live in its branches — soInvalidDataTypeExceptionfired before a single branch was evaluated. This made every nullable attribute unrepresentable in JSON:API-style documents, where resources are modelled asallOfcompositions. Composition and$refnodes now defer the decision to the branch or resolved target, which already evaluatesnullablecorrectly. Nulls no branch permits are still rejected, and now carry the failing branch's data path instead of a pathless error (#66). - The same rule was implemented nine times and the copies disagreed — only
two consulted
$ref, so a$refproperty that acceptednullin one code path was rejected in another. All nine now delegate to a singleSchemaValueNormalizer::allowsNull()helper, closing the divergence acrossproperties,items,prefixItems,dependentSchemas,if/then/else,not, and composition branches (#66).
Preparation for the 1.0.0 stable release. This section tracks work that
landed on dev after the 0.6.0 release cut. All entries below are
internal-only unless explicitly marked as public API.
Schema::fromConstraintGroups()named constructor — an alternative to the 56-parameterwithOverrides()constructor. The legacy constructor is retained (PHPDoc-only@deprecated, no runtime attribute) so the 5 internal callers do not trigger anE_DEPRECATEDcascade; it will be removed in 2.0 (per the internal schema-constructor ADR).- Post-refactor behavioral snapshot tests for 3 risk zones
(
tests/Integration/PostRefactorBehavioralSnapshotTest.php) covering discriminator annotation propagation, streaming-content parser edge cases, andoneOfnullable handling. - CI: PHP 8.4 matrix job and Infection mutation-testing job with MSI thresholds aligned to the current mutation score.
- Community-health files:
SECURITY.md, issue/PR templates,.github/dependabot.yml,.editorconfig. // §6 exemption:marker onFileExternalRefResolverdocumenting the ADR-gated decision to keep the class as a single 317-LOC unit (tightly coupled withRefResolverviaExternalRefResolverInterface).
- Internal decomposition (§6) — 16 oversized classes refactored into
cohesive collaborators. Public API signatures are unchanged; the splits
extract private/internal helpers and strategy objects:
OpenApiValidatorBuilder→ 3 internal collaborators.ValidatorCompiler→ scalar+pattern+utf16 and array+equality+object+ keyword internal collaborators.SchemaSiblingMerger→ 3 strategy collaborators (ScalarSiblingMerger,BoundSiblingMerger,CompositionSiblingMerger).StreamingContentParser→ 3 format-specific parsers (NdJsonParser,SseParser,JsonSeqParser) +StreamLineReader.RefResolver→UriResolver+DocumentNavigator+DiscriminatorDetector.- 4
SchemaParser/SchemaSerializeroversized classes decomposed. - 6 oversized
Validatorclasses: long methods extracted via extract-method.
- Validator signatures + DTO grouping (§10) —
EventDispatchingTraitrefactored; validator constructor signatures and DTO grouping aligned withSchemaValidatorDependenciesas the canonical DTO. - Codebase-wide code-style cleanup (386 violations from the
internal code-style audit, closed across 22 partitioned tasks):
- §3 Yoda canonical form applied across 28 sites in 17 files.
- §5 magic values extracted into named constants.
- §7 redundancy:
&$reference accumulators, duplicated code,sprintfconsolidation, loop-invariant hoists, regex normalization. - §9 nesting flattened via guard clauses; §4 naming aligned with
domain concepts; §10 exception
$code/$previousconvention standardised. - §12 forbidden PHPDoc removed across all modules (167 stale
restating-the-code comments).
ValidatorPool,PregExecutor, andLibxmlSecuredContextshrunk as a side effect;TypeFormatterdeleted (consolidated into its single remaining caller). - §11 silent catches now emit PSR-3 log entries at the boundary.
- AI-slop removal pass — dead
UriSchemeenum andAnyOfErrorexception class deleted;TypeCoercer4-predicate OR replaced withis_scalar();JsonEqualsandEnumScalarCacheboolean expressions extracted intoisNumeric()/isScalarOrNull()helpers; nested ternaries inScalarSchemaKeywordParserandPathItemBuilderflattened tomatch(true)/ early-return helpers. - Rector + PHP-CS-Fixer PHP 8.4 modernisation sweep applied.
Schema::withOverrides()(56-param constructor) — PHPDoc-only@deprecated since 1.x, will be removed in 2.0. UseSchema::fromConstraintGroups()orSchema::withOverrideGroups(). The#[Deprecated]attribute was replaced with a@deprecatedPHPDoc so the 5 internal callers do not trigger a runtimeE_DEPRECATEDcascade; the attribute will be reinstated when all internal callers migrate, or in 2.0 — whichever comes first.
Duyler\OpenApi\Validator\Schema\UriSchemeenum — speculative code, zero references insrc/ortests/.Duyler\OpenApi\Validator\Exception\AnyOfErrorexception class —AnyOfValidatorthrows plainValidationException(notAnyOfError), so the typed exception was never reachable from production code. StaleAnyOfErrorrow removed from the README Composition Errors table;DetailedFormatterTestfixture and dataset entry cleaned up.Duyler\OpenApi\Validator\TypeFormatter— consolidated into its single remaining caller during §12 PHPDoc cleanup.
nullis now accepted for$refproperties/items when the resolved target schema allowsnullviatype: [..., 'null'](previously only an explicitnullable: truesibling on the stub was honoured).NdJsonParsermemory regression on empty streams — the generator frame allocated byStreamLineReader::readLines()indirection added ~1.8 KB to the empty-stream parse path and broke theparse_stream_empty_ndjson_has_near_zero_memory_growthbudget. The parser now reads chunks directly from the PSR-7 stream; the string path still usesStreamLineReader::stripBom()for BOM handling.StreamingContentParser::parseStream()short-circuits empty streams viagetSize()before delegating, eliminating PCOV overhead on the empty-body path.NotValidator— removed redundant$schema->nottruthy check afteris_bool($schema->not)narrowing (PsalmRedundantCondition).- Composition branch errors are no longer lost, duplicated, or
inconsistent between keywords (#52). A
ValidationExceptionthrown byallOf/anyOf/oneOfnow always carries a structured error list: a value rejected during branch normalization (typicallynullagainst a non-nullable branch) synthesises aTypeMismatchErrorwith the branchdataPath/schemaPathinstead of an empty list;allOfno longer reports each branch error twice; and theallOfmessage counts the branches that did not match rather than one of two error buckets.BranchOutcomeandValidationResultcarry a single canonical error list (ValidationResult::$abstractErrorsmerged into$errors, new$failedCount), so no caller can double-count. - Infection CI job no longer OOMs; MSI thresholds realigned with the current mutation score so the gate is neither green-by-default nor unreachable.
- (nothing since 0.6.0)
0.6.0 - 2026-07-22
This release consolidates three production-readiness waves (R4, R3, and the
earlier SPEC/SEC series) into a single section. The R4 series closed 37
EI tickets across 18 tasks targeting correctness, security, spec compliance,
PSR-14, and regression coverage. The R3 series added missing format
validators, hardened the public exception surface, and closed another batch
of SPEC/SEC bugs. The older wave fixed JSON Schema 2020-12 §4.2.2 numeric
equality, $ref sibling merging, and JSON Pointer escaping.
- R4 regression test suites under
tests/Unit/Regression/R4/covering nested$ref, discriminator, YAML billion-laughs, info-leak, coercion,time/format ordering, cache keys, PSR-14 stoppable events (R4-TEST-004, R4-TEST-005, R4-TEST-006, R4-TEST-007, R4-TEST-008, R4-TEST-009, R4-TEST-010, R4-TEST-011, R4-TEST-012). EmailValidatoraccepts RFC 5321 domain literals, quoted local parts, and SMTPUTF8 addresses per RFC 6531 (R3-SPEC-010, C-010).- Registered 10 missing format validators in
BuiltinFormats(int32,int64, plus 8 others);enableStrictFormats()works on typical OpenAPI 3.2 specs (R3-SPEC-013, C-013). ValidatorPool::forCoroutineRuntime(object $lock, ?int $maxSize = null)named-constructor for Swoole-coroutine and FrankenPHP-threaded contracts (R3-CONCURRENCY-001).- Dedicated unit-test coverage for
WebhookValidator,Validation\CallbackValidator,ResponseValidationHandler(R3-TEST-003, R3-TEST-006, R3-TEST-007). - Property-based testing foundation under
tests/Property/usinggiorgiosironi/eris(R3-TEST-021). - CI matrix
.github/workflows/ci.ymladdsswoole_testsjob inphpswoole/swoole:php8.5withVerify Swoole extension loadedstep (R3-TEST-024, R3-TEST-025).
CallbackValidator(outer and inner) defaults$strictCallbackRuntimeTemplatetotrue, matching builder default (R3-SEC-009, R3-SEC-025, ASVS 4.0 V12.6.1, CWE-918, CWE-1188).ValidatorPool,LibxmlSecuredContext,PregExecutorcarry explicit@danger NOT_THREAD_SAFEmarker; README enumerates per-class mitigations (R3-CONCURRENCY-001, R3-CONCURRENCY-002, R3-CONCURRENCY-003).TypeCoercer::coerce()defaults to strict ($strict = true); legacy callers MUST passfalseexplicitly viadisableStrictCoercion()(TYPECOERCER-DEFAULT-STRICT).- Removed inline
//comments fromsrc/(72 lines across 15 files) perphp-best-practices.md§12 (R3-BP-003, BP-006).
SchemaValidatorandValidatorDependencies(the legacy stateless dispatcher and its dependency bag) are deprecated since 0.6.0. They are retained through the entire 1.x release line for backward compatibility with internal composition validators. Removal is scheduled for 2.0 as part of the parameter-validator migration. New code should useSchemaValidatorWithContext, the canonical validator wired internally byOpenApiValidatorBuilder::build(); direct callers can obtain it viaSchemaValidatorDependencies::rootSchemaValidator(). Direct construction ofValidatorDependenciesshould migrate toSchemaValidatorDependencies(the canonical DTO).
frankenphp_testsjob removed from.github/workflows/ci.yml. Extension is statically compiled into frankenphp binary, registered only in worker SAPI; CLI SAPI always skipped. Test class retained (R4-TEST-001).
R4 production-readiness series (37 EI tickets across 18 tasks). Highlights:
NumericRangeValidator::isMultipleOfno longer throws for valid int64 values whenbcmathnot loaded; pure-PHP decimal string-modulus added (R4-CORRECTNESS-008).DiscriminatorValidatorforks parentValidationContextand merges child annotations, sounevaluatedProperties/unevaluatedItemshonour target-validated items (R4-CORRECTNESS-005, R4-SPEC-015).TimeValidatorenforces RFC 3339 §5.6 offset (Z,+HH:MM,-HH:MM) forformat: time; offset-less strings now rejected (R4-SPEC-006).OneOfValidatorWithContext::hasNullableSchemaconsiders native nullable formtype: ["string", "null"](R4-CORRECTNESS-009).http/bearerscheme check uses end-anchored regex (/^bearer\s+\S+\s*$/i) to reject multi-challenge headers likeBearer fake, Basic ...(R4-SEC-016).coerceToInteger/coerceToIntegerStrictreject floats overflowing int64 nearPHP_INT_MAXviafloatExceedsInt64Range()(R4-SEC-013).NumberStringNormalizer::castStringToFloatOrFailrejects INF overflow (1e309) with clear reason instead of misleading precision-loss message (R4-SEC-014).coerceToIntegerStrictaccepts whole-valued floats (3.0) fortype: integerper §4.2.3 (R4-SPEC-005).oauth2/openIdConnect/http/basic/http/digest/mutualTLS/unknown scheme types throw newUnsupportedSecuritySchemeException(extends\RuntimeException) instead of misclassifying as missing credentials (R4-SEC-010). NOT wrapped intoValidationException.- OAuth2 scopes declared on security requirement forwarded to PSR-3 logger
at
debuglevel instead of discarded (R4-SPEC-003). - AND/OR semantics for mixed supported/unsupported security requirements honoured: AND-list with one unsupported fails closed; OR-list tries alternatives.
- Route
preg_matchinRegexValidator,PathParser,CallbackValidatorthroughPregExecutorso attacker patterns honourmaxRegexBacktracks(R4-SEC-002, R4-SEC-003, R4-SEC-004). - Resolve
$refinside all 13 schema-typed keywords before legacy-engine delegation; previously invalid data passed silently (R4-CORRECTNESS-001, R4-CORRECTNESS-016, R4-SEC-001, R4-SPEC-014, R4-ARCH-001). - Discriminator validator enumerates all composition arrays (
oneOf+anyOf+allOf), continues on unresolved nested candidates, and appliesdefaultMappingfallback (R4-CORRECTNESS-002, R4-CORRECTNESS-015, R4-SPEC-002, R4-SPEC-004). - ValidatorCompiler emits all supported keywords for nested
properties/items, inlinesJsonEqualsforenuminitems; unsupported keywords rejected at every depth viaUnsupportedKeywordException(R4-CORRECTNESS-004, R4-CORRECTNESS-013). ArrayDispatcher::dispatchrespects PSR-14StoppableEventInterface(R4-PSR-001).- Rebuild
FormatRegistryinbuild()so builtin format validators share configuredPregExecutorregardless ofwithFormat()order (R4-SEC-009). OpenApiValidatorBuilderadds parse-affecting config (maxSpecDepth/maxSpecSizeBytes/externalRefAllowedRoot/externalRefMaxBytes) toSchemaCachecache-key hash to prevent cross-caller cache-poisoning (R4-SEC-008, R4-SEC-017).
R3 production-readiness series:
JsonParserenforces configurable size cap (default 1 MB) before parsing, matchingYamlParser(R3-SEC-014, CWE-400, CWE-770).TypeCoercer::coerceToTypecoerces non-string inputs (bool/int/float) through typed coercion path (R3-CORRECTNESS-005).TypeCoercer::coerceUnionTypecatchesTypeMismatchErrorand continues to next type in union (R3-CORRECTNESS-006).UriValidatoraccepts all RFC 3986 valid URIs including scheme-less forms and non-allowlisted schemes (R3-SPEC-008, R3-SPEC-009).UriValidatorerror messages no longer disclose URI scheme/port (R3-SEC-021, CWE-209).PatternValidator::validate()callsRegexValidator::validate()before match, closing pattern-length cap gap (R3-SEC-002, ASVS 4.0 V5.3.4, CWE-1333, CWE-400, CWE-770).PregExecutorsetspcre.recursion_limit(default 512) alongsidepcre.backtrack_limitfor every call; the Swoole-race caveat from R3-SEC-020 now applies symmetrically to both ini variables (R3-SEC-017, R3-SEC-020, CWE-1333, CWE-400, CWE-770).SchemaSiblingMerger::merge()honours §8.2.3 ALL OF semantics for ten non-bound fields previously using sibling-wins (R3-SPEC-006, C-006).SchemaSiblingMerger::mergeSchemaOrBoolrecursively merges two Schema instances instead of silent sibling overwrite (R3-SPEC-019, O-042).SchemaacceptsSchema|bool|nullfor every schema-typed keyword (items/contains/propertyNames/if/then/else/not/unevaluatedItems) (R3-SPEC-005, C-005).UnevaluatedPropertiesValidatorconsults every adjacent in-place applicator viaValidationContextannotation propagation (R3-SPEC-001).UnevaluatedItemsValidator::getEvaluatedItemIndices(renamed) treatsitemsas evaluating every index>= prefixItems count(R3-SPEC-002).ContainsValidatorregisters matched indices inValidationContextviamarkItemEvaluated()(R3-SPEC-003).UnevaluatedItemsValidatorconsumes composition annotations viaevaluatedItemIndices()channel (R3-SPEC-004).ValidatorCompiler::generateConstCheckemits inlinejsonEqualscall instead of!==(R3-CORRECTNESS-001).ValidatorCompiler::generateEnumCheckemits linear-scan with inlinejsonEqualsinstead ofin_array(..., true)(R3-CORRECTNESS-002).ValidatorCompiler::generateArrayCheckforuniqueItemsemits canonicalised isset lookup (O(n)) plus100000cap, wrapsjson_encodein try/catch (R3-CORRECTNESS-003, R3-CORRECTNESS-013).ValidatorCompiler::generatePatternCheckdisambiguates PCRE error (false) from no-match (0) (R3-PERF-001).CompilationCache::generateKey()includes target PHP class name to close class-name collision; WeakMap widened toWeakMap<Schema, array<string, string>>(R3-ARCH-001).ValidatorPool::acquireLock()/releaseLock()no longer re-checkmethod_exists($this->lock, ...)on hot path (R3-CONCURRENCY-004, O-034).generateCacheKeyFromFileincorporates SHA-256 of spec contents to prevent cache-poisoning via size/mtime-preserving tampering (R3-SEC-003, S-003, ASVS V8.1.3, CWE-349, CWE-1023).
Older SPEC/SEC series:
JsonEquals::equals()rejects mixed int+float equality when int side outside IEEE 754 safe range viaSAFE_INT64_FLOAT_BOUNDARY = 2^53(PHP_INT_MAX-FLOAT-BOUNDARY).AbstractCoercer::coerceToInteger()(non-strict) rejects floats in boundary[PHP_INT_MAX - 1023, PHP_INT_MAX]and symmetric negative range via>= (float) PHP_INT_MAX(WEAKPHP-INT-CHECK).- JSON Pointer escape sequences (
~1for/,~0for~) now decoded in$reffragment resolution per RFC 6901 §3 (SPEC-01). $refsiblings evaluated alongside resolved schema per §8.2.3 via newSchemaSiblingMergerclass (SPEC-02).JsonEquals::equals()performs order-independent comparison for objects per §4.2.2 (SPEC-03), direct===for int-to-int to preserve int64 precision (SPEC-05), and handles bool vs int (SPEC-04).minLength/maxLengthcount UTF-16 code units per §4.2.1 via newUtf16::length()helper (SPEC-06); compiler inlines same logic.deepObjectparameter style handles bracket notation viaQueryParserrather than JSON decoding (SPEC-07).SchemaSiblingMerger::merge()aligns scalar bounds,nullable, composition keywords with §8.2.3 ALL OF semantics (SPEC-02B).ArrayLengthValidator::encodeArrayKey()canonicalizes array keys beforejson_encodeforuniqueItemsper §4.2.2 (SPEC-03B).- Documentation: the internal review-work process guide forbids working-tree mutation during review-work (RACE-CONDITION-PROCESS).
- All exception classes sanitize
__toString()via newSanitizableExceptionTrait, returning onlygetMessage()(CWE-209, CWE-497, S-019, R3-SEC-INFO-LEAK-SYSTEMATIC). - Category-B exception sensitive props moved to
protected readonlywith opt-in$e->value(reveal: true)(default'<redacted>'). Affects 5 classes incl.InvalidFormatException,InvalidParameterException(S-005, S-008, S-016, S-032). MalformedStreamRecordException::$recordtruncated to 256 bytes viaLogContextSanitizer::truncate()(S-007, CWE-209).FileExternalRefResolverpassesbasename()not absolute path inExternalRefSecurityException(S-005).UnresolvableRefExceptiontruncates$reason/$internalTraceto 256 bytes (S-008);ExternalRefSecurityException::$reftruncated similarly (S-005).UriValidatorno longer interpolates attacker-supplied scheme/port intoInvalidFormatExceptionmessages (R3-SEC-021, CWE-209).ValidatorCompilerescapes attacker-controlled property names in generated throw messages viavar_export($key, true), replacesaddslashes()(R3-SEC-015, CWE-094, CWE-133).YamlParserbillion-laughs defence: pre-parse capsMAX_ANCHORS = 100,MAX_ALIASES = 1000,MAX_ALIAS_DEPTH = 10run before Symfony parser (R4-SEC-012, CWE-400, CWE-770).- Sanitize
getMessage()inPathMismatchException/OperationNotFoundException/UnsupportedMediaTypeException/InvalidParameterExceptionso attacker paths/methods/Content-Types cannot leak (R4-SEC-007a/b/c/d, CWE-209, CWE-532). - Bounded streaming record count: NDJSON/SSE/JSON Text Sequences enforce
configurable cap (default 100 000) via
TooManyRecordsException(SEC-10, CWE-400, CWE-770). Configurable viawithMaxStreamingRecords(). - Hardened
FileExternalRefResolverscheme policy: onlyfile://URIs and scheme-less relative paths accepted (SEC-01, SEC-24). fromYamlFile()/fromJsonFile()auto-deriveallowedRoot = dirname(realpath($specPath))so external$refconfined to spec dir (SEC-03). Override viawithExternalRefAllowedRoot().assertPathWithinAllowedRoot()rejects sibling-directory bypass attempts (SEC-02, CWE-22, CWE-178).FileExternalRefResolver::resolve()usesfopen+fstat+fread+fclose; non-regular files rejected, 10 MB cap viaExternalRefTooLargeException+withExternalRefMaxBytes()(SEC-04, SEC-05, SEC-25).InvalidFormatException::params()omits raw user values (SEC-06, CWE-532); opt-in viawithDetailedErrors(includeSensitive: true).MissingSecurityCredentialsErroremits generic caller-safe message by default (SEC-07, CWE-209); details via opt-inwithSecurityVerboseLogging().ExternalRefSecurityExceptionandFileExternalRefResolverexception messages no longer interpolate$ref/absolute paths (SEC-08).UnresolvableRefException::getMessage()no longer interpolates attacker-controlled$refvalue (SEC-08-FULL, CWE-209).build()fails closed when string-loaded spec contains external$refandwithExternalRefAllowedRoot()not called (STRING-SPEC-FAILOPEN).- Coercion strict by default: boolean rejects unknown strings (SEC-13);
integer detects overflow (SEC-14); number rejects IEEE-754 precision loss
(SEC-15). Opt-out via
disableStrictCoercion(). ResponseHeadersValidator::coerceToNumberrejects numeric strings losing precision (RESPONSE-HEADERS-COERCION).http/bearermatching case-insensitive per RFC 6750 §2.1; trailing- space-only headers rejected (SEC-16).pattern/patternProperties/propertyNamescapped atRegexValidator::MAX_PATTERN_LENGTH = 1024bytes (SEC-11, CWE-1333, CWE-770).@operator replaced withset_error_handler(SEC-12).JsonBodyParser/JsonParserreject invalid UTF-8 viamb_check_encoding()beforejson_decode()(SEC-17, RFC 8259 §8.1).YamlParser::parseContent()enforces size cap (default 1 MB) and depth cap (default 100) (SEC-18). Builder:withMaxSpecSize()/withMaxSpecDepth().- Leap second (
:60) indate-time/timerestricted to UTC end-of-day (SEC-20, SEC-21).
SecurityValidatorthrows newUnsupportedSecuritySchemeException(extends\RuntimeException, NOTValidationException) for unsupported scheme types (oauth2/openIdConnect/http/basic/http/digest/mutualTLS/unknown) (R4-SEC-010, R4-SPEC-003).- Strict callback runtime template default flipped to opt-out:
{$request.body#/callback_url}throwsUnresolvableCallbackPathException(SEC-09, CWE-918, CWE-1188). Opt back viadisableStrictCallbackRuntimeTemplate(). - Exception sanitisation: direct property access (
$e->value,$e->ref,$e->schemeName,$e->parameterName, etc.) throwsError: Cannot access protected property. Use opt-in getter ($e->value(reveal: true)). - R4-SEC-007a/b/c/d message changes break callers string-matching
getMessage()ofPathMismatchException/OperationNotFoundException/UnsupportedMediaTypeException/InvalidParameterException. Use public readonly properties or$e->parameterName(reveal: true)instead. ValidatorCompiler::generatePatternCheckemits inlined defensivepreg_matchwrapper forpattern(R3-SEC-001, ASVS 4.0 V5.3.4, CWE-1333, CWE-400). Operators MUST flushCompilationCachepool to retire cached validators with rawpreg_match.CompilationCache::generateKey()adds document context for schemas with$ref, closing cross-document poisoning (R3-SEC-004). Schemas with$refbut no$documentthrowCompilationCacheException(fail-closed). Signature:(Schema, string, ?OpenApiDocument = null).SchemaCachekey incorporates parse-config fingerprint (R4-SEC-008, R4-SEC-017). Existing entries cache-miss on firstbuild()after upgrade; pre-warm at deploy time if cold-start cost matters.format: int32/int64registered viaIntegerRangeValidator; out-of-range values now throwInvalidFormatExceptionfrom the format validator BEFOREmaximum/minimum(R3-SPEC-013, C-013).
0.5.0 - 2026-07-18
This release focuses on defense-in-depth security hardening, spec-compliance
bug fixes, and internal decomposition of the two largest god classes. The
public API is preserved with two additions: Operation now carries resolved
path parameters and the optional enableStrictCallbackRuntimeTemplate() mode
fail-closes on unresolvable callback expressions.
- Closed CWE-94 code injection via type field in
ValidatorCompiler; generated code now rejects class-name injection. - Added
PregExecutorwrapper enforcing a defensivepcre.backtrack_limit(configurable viawithMaxRegexBacktracks()) to prevent ReDoS on attacker-controlled JSON-Schemapatternfields. - Capped request body and streaming payload sizes via
withMaxJsonBodySize()andwithMaxMultipartBodySize(); streaming depth limit and opt-inenableStrictStreaming()mode raiseMalformedStreamRecordExceptioninstead of silently skipping bad records. - Replaced xxHash64 cache keys with SHA-256 and
spl_object_idwithWeakMapto resist hash-collision poisoning and object-id reuse. - Closed auth fail-open in
CookieValidatorand added opt-inenableStrictCallbackRuntimeTemplate()to reject unresolvable callback runtime expressions (e.g.{$request.body#/callback_url}). UnresolvableRefException::getMessage()no longer discloses the internal circular-reference traversal path; the path is preserved in a readonly$internalTraceproperty for safe logging.AbstractCompositionalValidatorcaps accumulated errors at 20;ContainsValidatorcaps validations at 10 000;ArrayLengthValidatorabortsuniqueItemsafter 100 000 entries.XmlBodyParserenforces a 1 MB defaultmaxXmlByteslimit (configurable).- Added
LogContextSanitizerto truncate and escape untrusted strings before they enter PSR-3 context, mitigating log injection. - Added builtin
FileExternalRefResolverwith SSRF protection: network schemes (http://,https://,ftp://,ftps://) are denied by default; optionalallowedRootdefends against path traversal and symlink escapes.
OperationDTO now exposes resolvedpathParameters,operationId, and nullableschemaOperationreference; existing call sites keep working via defaults.OpenApiValidatorInterface::getDocument()promoted to the public interface.resolveLinkWithContext()andLinkContextfor full OpenAPI 3.2 Runtime Expression support ($url,$method,$statusCode,$request.*,$response.*).- Strict streaming and strict callback runtime template modes (see Security).
- Magic-value enums (
UriScheme,ContentMediaType,BooleanCoercionValue,XmlNodeType) replace stringly-typed comparisons.
- Decomposed
Schemagod class into focused constraint sub-DTOs (ArrayConstraints,CompositionConstraints,NumericConstraints,ObjectConstraints,StringConstraints). - Decomposed
OpenApiBuildergod class into focused builder modules (ComponentsBuilder,InfoBuilder,PathItemBuilder,SchemaBuilder,SecuritySchemeBuilder) resolved lazily throughOpenApiBuildContext. - Introduced
ValidatorDependenciesDTO and a lazy registry forValidatorFactory(open-closed principle). ValidationContextis now per-request and renamed to avoid the name clash withError\ValidationContext.- Split overloaded boolean-flag builder methods into focused
enable*/disable*pairs. - Consolidated ~660 lines of redundant PHPDoc, dead defensive guards, and
unreachable branches across
src/. Public API signatures preserved.
- Reuse
SchemaValidatoron the hot path; cached discriminator and$refdetection. RefResolvercache,PathFindertrie accumulator, and route sort precompilation.- Coercion hot path,
EnumScalarCacheHashSet, and pattern normalization caches. PathFindermigrated from linear scan to a segment-based trie.
- Restored libxml external entity loader and removed global-state mutation
from
ValidatorPool::reset()(Swoole / FrankenPHP threaded workers). - SSE parser now handles no-colon lines per WHATWG and supports dotted RFC 6570 variable names.
- Nine JSON Schema / OAS spec compliance bugs: whole-float acceptance for
type: integer,multipleOfepsilon equivalence,const/enumnumeric equality,required/allowEmptyValueorthogonality, and more. - Five composition validator bugs in
allOf/anyOf/oneOf/not: typed-error propagation,$refresolution, and discriminator double validation. - Replaced
@operator inPatternValidatorwith explicit error handling; addedTypeFormatterutility for typed error messages. SchemaRegistrysemantics refined;PathFinderexception naming aligned with public API.- README parity fixes: qualified performance claims, documented PHP runtime caveats, and aligned public API examples.
ValidationErrorInterface::getType()is deprecated in favor ofkeyword(). Both return the same value;getType()will be removed in 2.0.
- Added concurrency tests for Swoole, FrankenPHP threaded, and RoadRunner prefork runtimes.
- Added ReDoS, billion-laughs, deep-nesting, and memory-exhaustion security regression tests.
- Added cache-invalidation and alternate PSR-7 implementation coverage (Guzzle, Laminas Diactoros).
0.4.1 - 2026-07-16
- OAS 3.1 type-array null handling:
SchemaValueNormalizernow recognizestype: [string, "null"]as nullable in all validator call sites (bug A1 from PR #25 analysis). FormatValidatorno longer crashes whennullis the first element in a type-array; it uses thefirstStringType()helper (bug A2).BodyParsernow parsesapplication/vnd.api+json(JSON:API) as JSON instead of treating it as a raw string (bug B1).
SchemaValueNormalizer::typeIncludesNull()helper for OAS 3.1 type-array null detection.- TypeValidator regression tests for
nullableAsType+ type-array interaction.
0.4.0 - 2026-07-15
- Hardened XML parsing against XXE: deny-all libxml entity loader, internal error state save/restore, and non-network loading.
- Removed global-state races in date/time and JSON parsing by using
DateTimeImmutable/regex andJSON_THROW_ON_ERRORwith capped depth, eliminating reliance on shared error globals. - Hardened request and stream parsing: case-insensitive
Content-Type, rejection ofq-value=0, multipart boundary extraction from the header, UTF-8 BOM stripping, line/record length limits, query/cookie pair-count limits, and untrusted JSON depth limits. - Added optional lock support to
ValidatorPoolfor Swoole/coroutine runtimes, and converted static regex and path caches to instance-bound LRU caches withreset()clearing. - Prevented injection and traversal:
preg_quotein path-template regex,var_export/class-name validation in compiled validator codegen, correct RFC 6901 escaping in JSON Pointer resolution, and discriminator mapping by schema name. - Hardened schema coercion and numeric handling: strict integer/number/boolean
coercion, whole-float acceptance for
type: integer, numeric equality inconst/enum,multipleOfgreater-than-zero and zero guards, and orthogonalrequired/allowEmptyValuehandling. - Bounded memory and error output: capped additional-property errors, O(n)
hash-map deduplication for
uniqueItems, and bounded streaming buffers. - Replaced MD5 with xxHash64 for cache key generation.
- Opt-in security validation for
http/bearerandapiKey(header/query/cookie) schemes viaenableSecurityValidation(). - Opt-in server path resolution via
enableServerPathResolution(); supports server variables, relative URLs, and longest-prefix matching. - Full Runtime Expression support in links:
resolveLinkWithContext()withLinkContextfor$response.header,$response.query,$url,$method, and$statusCode. validateWebhook()andvalidateCallback()public methods for webhook and callback request validation.ContentEncodingValidatorandContentMediaTypeValidatorfor base64 and JSON/XML/text media types.ExampleValidatorfor optional, non-blocking validation ofexample/examples.ValidatorCompilerguards against unsupported keywords and supportsreadOnly/writeOnlyvalidation.- OpenAPI 3.1+
nullableconversion totype: [..., "null"]while preserving OpenAPI 3.0 boolean behavior. - Server URL validation and strict format mode (
enableStrictFormats()) with PSR-3 warnings. - Runtime deprecation warnings and version-aware
exclusiveMinimum/exclusiveMaximumhandling. - Validation lifecycle events (
ValidationStartedEvent,ValidationFinishedEvent,ValidationErrorEvent) and PSR-3 logger integration. PathItem::getOperation()for consolidated HTTP method routing.ValidatorPoolLRU cache with configurable size andclear()for hot reload.ExternalRefResolverInterfacefor custom external$refresolution.SchemaRegistry::getOrFail()for fail-fast schema lookups.- Boolean schema support (
type: boolean).
- Refactored
OpenApiValidatorinto a facade backed by specialized handlers; public interface preserved. OpenApiValidatorBuildernow returnsOpenApiValidatorInterfaceand delegates to a shared internalwith()method.BuilderExceptionnow extendsRuntimeException.- Added
Schema::withOverrides()for non-destructive schema overrides. - Extracted
CompilationCacheInterfaceandRequestValidatorInterface. - Coercion and security validation now use internal context DTOs.
- Event dispatching extracted into a shared trait.
- Added
finalto concrete classes and updated PHPDoc/version annotations.
- Cached regex patterns in
PathRegexCacheandRegexValidatorwith instance-bound LRU eviction. - Shared
ValidatorRegistryandStatelessValidatorRegistry; cached enum checks and error merging. - Optimized
ValidatorPool::touch()to O(1) and rewroteRefResolver::navigate()iteratively. PathFinderuses a segment-based trie instead of linear scan;PathParser::tryMatchPath()avoids exception-based control flow.- Made
BreadcrumbManagerandValidationContextmutable for the hot validation path. - Moved stateless validators and body parsers to constructors for reuse.
- Streaming parsers read PSR-7 streams in bounded chunks.
- Cache schema hashes, placeholder counts, and compiled validator results once per call.
SchemaValidatorAdapternow passesErrorValidationContextsodisableNullableAsType()andwithEmptyArrayStrategy()are honored for plain schemas.- Parameter validators now propagate
nullableAsTypeandemptyArrayStrategythroughParameterValidationConfig. PrefixItemsValidatorno longer re-validates remaining items;ContainsValidatorwas merged with range handling.ResponseTypeCoercerpreserves null properties and additional properties;coerceToArrayreturns non-arrays as-is.NotValidator,OneOfValidator, andPropertiesValidatorpropagate typed errors intoValidationException.AdditionalPropertiesValidatoremitsAdditionalPropertyErrorinstead ofUnevaluatedPropertyError, and caps error count.JsonBodyParserreturnsnullfor empty bodies;CookieValidatorpreserves flag-style cookies;QueryParserhandles RFC 6570 form/explode semantics, literal key names, and nesting limits.ParameterDeserializeradds deepObject style support andFormBodyParserdelegates toQueryParser.EmailValidatorrequires a TLD,HostnameValidatorsupports IDN and label rules,ByteValidatoraccepts URL-safe base64,UriValidatoruses RFC 3986 scheme/port checks, andUuidValidatorsupports RFC 9562 v6/v7/v8 and nil UUID.XmlBodyParserpreserves attributes, repeated elements, empty elements, and mixed content.ResponseHeadersValidatorfails closed on unknown boolean values and coerces empty header values to objects when requested.ResponseValidationHandlerfalls back to webhooks for webhook response cycles.- Streaming: SSE handles CRLF/CR/LF line endings, default event name, multi-line data, and retry; NDJSON supports CRLF; BOM stripped from all JSON/XML streams.
ValidatorCompilernow matches runtime behavior fortype: integerandmultipleOf, caches schema hashes once, guards TTL, and wraps JSON exceptions.RefResolverenforces recursion depth limits, resolves parameters/responses, and handles external refs via optionalExternalRefResolverInterface.SchemaValidatorAdapterroutes discriminator and$refschemas throughSchemaValidatorWithContext.$refresolution inallOf/oneOfcompositions and callback runtime expressions fixed.- Wildcard media types expanded for request bodies; Content-Type matching is case-insensitive.
- Query parameters treat
requiredandallowEmptyValueas orthogonal. - Added required request body check and eliminated discriminator double validation.
LinkResolverhandles out-of-bounds list indices in link resolution.
0.3.3 - 2026-05-16
- Set
symfony/yamlrequirement to^7.0.