Skip to content

[php-nextgen] Add anyof Support#24306

Open
ckoegel wants to merge 19 commits into
OpenAPITools:masterfrom
ckoegel:php-nextgen-anyof
Open

[php-nextgen] Add anyof Support#24306
ckoegel wants to merge 19 commits into
OpenAPITools:masterfrom
ckoegel:php-nextgen-anyof

Conversation

@ckoegel

@ckoegel ckoegel commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Follow up to #23985
This PR adds support for anyOf models with and without discriminators. It copies the implementation from the oneOf model and updates some of the naming/logic to refer to Composed instead of oneOf or anyOf specifically.

PR checklist

  • Read the contribution guidelines.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    (For Windows users, please run the script in WSL)
    Commit all changed files.
    This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
    These must match the expectations made by your contribution.
    You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example ./bin/generate-samples.sh bin/configs/java*.
    IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
  • If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.

@jebentier (2017/07), @dkarlovi (2017/07), @mandrean (2017/08), @jfastnacht (2017/09), @ybelenko (2018/07), @renepardon (2018/12)


Summary by cubic

Adds anyOf support to the PHP Nextgen generator with a new AnyOfInterface and generated dispatcher models. Updates ObjectSerializer to resolve oneOf/anyOf right after json_decode and before the array-to-object cast/normalization to keep arrays and scalars intact.

  • New Features

    • Generate anyOf dispatcher models via model_anyof.mustache; add AnyOfInterface and register it in PhpNextgenClientCodegen.
    • Add deserializeAnyOf() and dispatch for both OneOfInterface and AnyOfInterface in ObjectSerializer.
    • Update model docs to list anyOf members and discriminator selection; skip per-property test stubs for composed models; add anyOf tests; regenerate samples/docs/tests.
  • Bug Fixes

    • Dispatch oneOf/anyOf before the array-to-object cast and normalization in ObjectSerializer to avoid corrupting arrays/scalars.
    • Catch Throwable during deserialization.
    • Minor comment and test cleanups.

Written for commit 457f0f4. Summary will update on new commits.

Review in cubic

@ckoegel
ckoegel marked this pull request as ready for review July 15, 2026 14:38

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 35 files

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread samples/client/echo_api/php-nextgen/src/ObjectSerializer.php Outdated
Comment thread samples/client/petstore/php-nextgen/OpenAPIClient-php/tests/Model/ReptileTest.php Outdated
Comment thread samples/client/petstore/php-nextgen/OpenAPIClient-php/tests/Model/ReptileTest.php Outdated
Comment thread samples/client/petstore/php-nextgen/OpenAPIClient-php/tests/Model/ReptileTest.php Outdated
Comment thread samples/client/echo_api/php-nextgen/src/ObjectSerializer.php Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 5 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

supportingFiles.add(new SupportingFile("ObjectSerializer.mustache", toSrcPath(invokerPackage, srcBasePath), "ObjectSerializer.php"));
supportingFiles.add(new SupportingFile("ModelInterface.mustache", toSrcPath(modelPackage, srcBasePath), "ModelInterface.php"));
supportingFiles.add(new SupportingFile("OneOfInterface.mustache", toSrcPath(modelPackage, srcBasePath), "OneOfInterface.php"));
supportingFiles.add(new SupportingFile("ComposedInterface.mustache", toSrcPath(modelPackage, srcBasePath), "ComposedInterface.php"));

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about keeping the OneOfInterface and adding a AnyOfInterface instead?

Renaming OneOfInterface to ComposedInterface is a breaking change if I'm not mistaken.

For other generators we do maintain separate implementations (mustache files) for anyOf and oneOf.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm definitely not opposed to splitting them out, I implemented it this way since the two are so similar to avoid some more duplication. But if we want to match some of the other generators we can definitely migrate to 2 separate models.

This is technically breaking, but the original oneOf implementation hasn't been released yet, so the only people this would break is those using 7.24.0-SNAPSHOT. If that's an issue then we can definitely break these out into oneOf/anyOf. Let me know and I can start on that

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@coffeemakr can you please also review this change when you've time since this is a follow-up PR to #23985 authored by you?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I prefer making the implementation more consistent to other generators. In addition, the term Composed may not be familiar to developers who just start using OpenAPI in their workflow.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, I'll get started on this rn

@wing328

wing328 commented Jul 16, 2026

Copy link
Copy Markdown
Member

also cc @coffeemakr @JulianVennen (top contributors to php-nextgen client generator)

@wing328 wing328 modified the milestones: 7.24.0, 7.25.0 Jul 18, 2026
@ckoegel
ckoegel marked this pull request as draft July 21, 2026 18:17
@ckoegel
ckoegel marked this pull request as ready for review July 21, 2026 19:24

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 36 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="modules/openapi-generator/src/main/resources/php-nextgen/ObjectSerializer.mustache">

<violation number="1" location="modules/openapi-generator/src/main/resources/php-nextgen/ObjectSerializer.mustache:501">
P1: Nested `anyOf` string values fail to deserialize because arbitrary property strings are JSON-decoded before dispatch. Preserve non-JSON strings (or dispatch before this decode) so an `anyOf` containing `string` can receive its original value.</violation>
</file>

<file name="samples/client/petstore/php-nextgen/OpenAPIClient-php/src/ObjectSerializer.php">

<violation number="1" location="samples/client/petstore/php-nextgen/OpenAPIClient-php/src/ObjectSerializer.php:641">
P1: `anyOf` responses matching multiple members lose data because this returns the first valid member. Preserve all matching member data (or generate a composed representation); `Smoothie` with only `lengthCm` currently becomes an empty `Apple`.</violation>
</file>

<file name="samples/client/echo_api/php-nextgen-streaming/src/ObjectSerializer.php">

<violation number="1" location="samples/client/echo_api/php-nextgen-streaming/src/ObjectSerializer.php:632">
P2: Scalar anyOf members are selected by declaration order rather than JSON type: `settype()` coerces every non-null scalar, so `[integer, string]` deserializes `"abc"` as `0`. Validate a primitive candidate before coercion, then continue to the next member when its input type does not match.</violation>
</file>

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

return self::deserializeOneOf($data, $class, $httpHeaders);
}
if (is_subclass_of($class, '\{{modelPackage}}\AnyOfInterface')) {
return self::deserializeAnyOf($data, $class, $httpHeaders);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Nested anyOf string values fail to deserialize because arbitrary property strings are JSON-decoded before dispatch. Preserve non-JSON strings (or dispatch before this decode) so an anyOf containing string can receive its original value.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/resources/php-nextgen/ObjectSerializer.mustache, line 501:

<comment>Nested `anyOf` string values fail to deserialize because arbitrary property strings are JSON-decoded before dispatch. Preserve non-JSON strings (or dispatch before this decode) so an `anyOf` containing `string` can receive its original value.</comment>

<file context>
@@ -492,14 +492,18 @@ class ObjectSerializer
                 return self::deserializeOneOf($data, $class, $httpHeaders);
             }
+            if (is_subclass_of($class, '\{{modelPackage}}\AnyOfInterface')) {
+                return self::deserializeAnyOf($data, $class, $httpHeaders);
+            }
+
</file context>


if ($instance instanceof ModelInterface) {
if ($instance->valid()) {
return $instance;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: anyOf responses matching multiple members lose data because this returns the first valid member. Preserve all matching member data (or generate a composed representation); Smoothie with only lengthCm currently becomes an empty Apple.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/php-nextgen/OpenAPIClient-php/src/ObjectSerializer.php, line 641:

<comment>`anyOf` responses matching multiple members lose data because this returns the first valid member. Preserve all matching member data (or generate a composed representation); `Smoothie` with only `lengthCm` currently becomes an empty `Apple`.</comment>

<file context>
@@ -587,6 +598,56 @@ private static function deserializeOneOf(mixed $data, string $class, ?array $htt
+
+            if ($instance instanceof ModelInterface) {
+                if ($instance->valid()) {
+                    return $instance;
+                }
+            } elseif ($instance !== null) {
</file context>


foreach ($class::getAnyOfTypes() as $type) {
try {
$instance = self::deserialize($data, $type, $httpHeaders);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Scalar anyOf members are selected by declaration order rather than JSON type: settype() coerces every non-null scalar, so [integer, string] deserializes "abc" as 0. Validate a primitive candidate before coercion, then continue to the next member when its input type does not match.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/echo_api/php-nextgen-streaming/src/ObjectSerializer.php, line 632:

<comment>Scalar anyOf members are selected by declaration order rather than JSON type: `settype()` coerces every non-null scalar, so `[integer, string]` deserializes `"abc"` as `0`. Validate a primitive candidate before coercion, then continue to the next member when its input type does not match.</comment>

<file context>
@@ -588,6 +599,56 @@ private static function deserializeOneOf(mixed $data, string $class, ?array $htt
+
+        foreach ($class::getAnyOfTypes() as $type) {
+            try {
+                $instance = self::deserialize($data, $type, $httpHeaders);
+            } catch (\Throwable $e) {
+                // Any failure means the data does not match this member type; try the next one.
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants