Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php
/**
* AnyOfInterface
*
* PHP version 8.1
*
* @package {{modelPackage}}
* @author OpenAPI Generator team
* @link https://openapi-generator.tech
*/

{{>partial_header}}

/**
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
* https://openapi-generator.tech
* Do not edit the class manually.
*/

namespace {{modelPackage}};

/**
* Interface implemented by models generated from an `anyOf` schema.
*
* An `anyOf` schema is not represented by a value object; instead a value is one of the
* member types. Classes implementing this interface only carry the metadata that
* {@see ObjectSerializer::deserialize()} needs to resolve the concrete member type.
*
* @package {{modelPackage}}
* @author OpenAPI Generator team
*/
interface AnyOfInterface
{
/**
* List of the types a value of this `anyOf` schema may be. Each entry uses the same
* notation as the values of {@see ModelInterface::openAPITypes()}.
*
* @return string[]
*/
public static function getAnyOfTypes(): array;

/**
* Name of the discriminator property used to resolve the concrete member type, or null
* when the schema has no discriminator.
*
* @return string|null
*/
public static function getAnyOfDiscriminator(): ?string;

/**
* Mapping of discriminator values to the concrete member type. Empty when the schema has
* no discriminator.
*
* @return array<string,string>
*/
public static function getAnyOfDiscriminatorMappings(): array;
}
Original file line number Diff line number Diff line change
Expand Up @@ -490,16 +490,24 @@ class ObjectSerializer
}
return $data;
} else {
$data = is_string($data) ? json_decode($data) : $data;

if (is_array($data)) {
$data = (object) $data;
if (is_string($data)) {
$decoded = json_decode($data);
// Keep the original string if decode fails (nested values are already PHP strings).
$data = ($decoded !== null || $data === 'null') ? $decoded : $data;
}

// A oneOf schema is not a value object: resolve the data to one of its member types.
// A oneOf/anyOf schema is not a value object: dispatch here, after json_decode (so
// members get the decoded value) but before the array-to-object cast (which breaks arrays).
if (is_subclass_of($class, '\{{modelPackage}}\OneOfInterface')) {
return self::deserializeOneOf($data, $class, $httpHeaders);
}
if (is_subclass_of($class, '\{{modelPackage}}\AnyOfInterface')) {
return self::deserializeAnyOf($data, $class, $httpHeaders);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
}

if (is_array($data)) {
$data = (object) $data;
}

// If a discriminator is defined and points to a valid subclass, use it.
$discriminator = $class::DISCRIMINATOR;
Expand Down Expand Up @@ -536,34 +544,61 @@ class ObjectSerializer
}
}

/**
* Returns true when $data's PHP type is compatible with the OpenAPI primitive $type.
* Prevents settype() coercion (e.g. "abc" → 0 for integer) from producing a false match
* when iterating over oneOf/anyOf candidate types.
* Non-primitive types always return true and are validated downstream by ModelInterface::valid().
*/
private static function isPrimitiveTypeCompatible(mixed $data, string $type): bool
{
return match($type) {
'int', 'integer' => is_int($data),
'float', 'double', 'number' => is_int($data) || is_float($data),
'bool', 'boolean' => is_bool($data),
'string' => is_string($data),
default => true,

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: oneOf/anyOf with an array member can still select that member for scalar input: default => true allows it, then settype($data, 'array') produces a non-null array. Add structural checks for array (and object) before the fallback so member order cannot coerce a scalar into a match.

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 560:

<comment>`oneOf`/`anyOf` with an `array` member can still select that member for scalar input: `default => true` allows it, then `settype($data, 'array')` produces a non-null array. Add structural checks for `array` (and `object`) before the fallback so member order cannot coerce a scalar into a match.</comment>

<file context>
@@ -540,6 +544,23 @@ class ObjectSerializer
+            'float', 'double', 'number' => is_int($data) || is_float($data),
+            'bool', 'boolean' => is_bool($data),
+            'string' => is_string($data),
+            default => true,
+        };
+    }
</file context>
Suggested change
default => true,
'array' => is_array($data),
'object' => is_object($data),
default => true,

};
}

/**
* Deserialize data into one of the member types of a `oneOf` schema.
*
* When the schema declares a discriminator, its value selects the member type directly.
* Otherwise each member type is tried in turn and the first one that yields a valid model
* (or a non-null primitive) is returned.
*
* @param mixed $data the data already decoded to an object
* @param mixed $data the decoded data to resolve to a member type
* @param string $class a class name implementing OneOfInterface
* @param string[]|null $httpHeaders HTTP headers
*
* @return mixed an instance of one of the `oneOf` member types
*/
private static function deserializeOneOf(mixed $data, string $class, ?array $httpHeaders): mixed
{
// $data is already decoded (see deserialize()). Read the discriminator from an object view
// of it, but deserialize each member from the original $data so array and scalar members
// keep their own type handling instead of being coerced to an object here.
$probe = is_array($data) ? (object) $data : $data;

$discriminator = $class::getOneOfDiscriminator();
if ($discriminator !== null && isset($data->{$discriminator}) && is_string($data->{$discriminator})) {
if ($discriminator !== null && is_object($probe) && isset($probe->{$discriminator}) && is_string($probe->{$discriminator})) {
$mappings = $class::getOneOfDiscriminatorMappings();
if (isset($mappings[$data->{$discriminator}])) {
return self::deserialize($data, $mappings[$data->{$discriminator}], $httpHeaders);
if (isset($mappings[$probe->{$discriminator}])) {
return self::deserialize($data, $mappings[$probe->{$discriminator}], $httpHeaders);
}
}

foreach ($class::getOneOfTypes() as $type) {
if (!self::isPrimitiveTypeCompatible($data, $type)) {
continue;
}
try {
$instance = self::deserialize($data, $type, $httpHeaders);
} catch (\Throwable $e) {
// The data does not match this member type, try the next one.
// Any failure means the data does not match this member type; try the next one.
// The broad catch is intentional: a real failure is not hidden, since the loop
// throws below when no member type matches.
continue;
}

Expand All @@ -579,6 +614,59 @@ class ObjectSerializer
throw new \InvalidArgumentException(sprintf('No matching schema in oneOf %s for the given data', $class));
}

/**
* Deserialize data into one of the member types of an `anyOf` schema.
*
* When the schema declares a discriminator, its value selects the member type directly.
* Otherwise each member type is tried in turn and the first one that yields a valid model
* (or a non-null primitive) is returned.
*
* @param mixed $data the decoded data to resolve to a member type
* @param string $class a class name implementing AnyOfInterface
* @param string[]|null $httpHeaders HTTP headers
*
* @return mixed an instance of one of the `anyOf` member types
*/
private static function deserializeAnyOf(mixed $data, string $class, ?array $httpHeaders): mixed
{
// $data is already decoded (see deserialize()). Read the discriminator from an object view
// of it, but deserialize each member from the original $data so array and scalar members
// keep their own type handling instead of being coerced to an object here.
$probe = is_array($data) ? (object) $data : $data;

$discriminator = $class::getAnyOfDiscriminator();
if ($discriminator !== null && is_object($probe) && isset($probe->{$discriminator}) && is_string($probe->{$discriminator})) {
$mappings = $class::getAnyOfDiscriminatorMappings();
if (isset($mappings[$probe->{$discriminator}])) {
return self::deserialize($data, $mappings[$probe->{$discriminator}], $httpHeaders);
}
}

foreach ($class::getAnyOfTypes() as $type) {
if (!self::isPrimitiveTypeCompatible($data, $type)) {
continue;
}
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.
// The broad catch is intentional: a real failure is not hidden, since the loop
// throws below when no member type matches.
continue;
}

if ($instance instanceof ModelInterface) {
if ($instance->valid()) {
return $instance;
}
} elseif ($instance !== null) {
return $instance;
}
}

throw new \InvalidArgumentException(sprintf('No matching schema in anyOf %s for the given data', $class));
}

/**
* Build a query string from an array of key value pairs.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
namespace {{modelPackage}};
{{^isEnum}}
{{^oneOf}}
{{^anyOf}}

{{^parentSchema}}
use ArrayAccess;
Expand All @@ -30,6 +31,7 @@ use JsonSerializable;
use InvalidArgumentException;
use ReturnTypeWillChange;
use {{invokerPackage}}\ObjectSerializer;
{{/anyOf}}
{{/oneOf}}
{{/isEnum}}

Expand All @@ -45,10 +47,12 @@ use {{invokerPackage}}\ObjectSerializer;
{{^parentSchema}}
{{^isEnum}}
{{^oneOf}}
{{^anyOf}}
* @implements ArrayAccess<string, mixed>
{{/anyOf}}
{{/oneOf}}
{{/isEnum}}
{{/parentSchema}}
*/
{{#isEnum}}{{>model_enum}}{{/isEnum}}{{^isEnum}}{{#oneOf}}{{#-first}}{{>model_oneof}}{{/-first}}{{/oneOf}}{{^oneOf}}{{>model_generic}}{{/oneOf}}{{/isEnum}}
{{#isEnum}}{{>model_enum}}{{/isEnum}}{{^isEnum}}{{#oneOf}}{{#-first}}{{>model_oneof}}{{/-first}}{{/oneOf}}{{^oneOf}}{{#anyOf}}{{#-first}}{{>model_anyof}}{{/-first}}{{/anyOf}}{{^anyOf}}{{>model_generic}}{{/anyOf}}{{/oneOf}}{{/isEnum}}
{{/model}}{{/models}}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
class {{classname}} implements AnyOfInterface
{
/**
* The original name of the model.
*
* @var string
*/
public const MODEL_NAME = '{{name}}';

/**
* The discriminator property name, or null when the schema has no discriminator.
*
* @var string|null
*/
public const DISCRIMINATOR = {{#discriminator}}'{{propertyBaseName}}'{{/discriminator}}{{^discriminator}}null{{/discriminator}};

/**
* {@inheritdoc}
*/
public static function getAnyOfTypes(): array
{
return [
{{#composedSchemas}}{{#anyOf}}'{{{dataType}}}'{{^-last}},
{{/-last}}{{/anyOf}}{{/composedSchemas}}
];
}

/**
* {@inheritdoc}
*/
public static function getAnyOfDiscriminator(): ?string
{
return self::DISCRIMINATOR;
}

/**
* {@inheritdoc}
*/
public static function getAnyOfDiscriminatorMappings(): array
{
return [
{{#discriminator}}{{#mappedModels}}'{{mappingName}}' => '\{{modelPackage}}\{{modelName}}'{{^-last}},
{{/-last}}{{/mappedModels}}{{/discriminator}}
];
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,31 @@
{{#models}}{{#model}}# {{classname}}
{{#oneOf.isEmpty}}
{{#anyOf.isEmpty}}

## Properties

Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------
{{#vars}}**{{name}}** | {{#isPrimitiveType}}**{{{dataType}}}**{{/isPrimitiveType}}{{^isPrimitiveType}}[**{{{dataType}}}**]({{complexType}}.md){{/isPrimitiveType}} | {{description}} |{{^required}} [optional]{{/required}}{{#isReadOnly}} [readonly]{{/isReadOnly}}{{#defaultValue}} [default to {{{.}}}]{{/defaultValue}}
{{/vars}}
{{/anyOf.isEmpty}}
{{^anyOf.isEmpty}}

This model is an `anyOf` wrapper: a value is at least one of the member types listed below.
It is never instantiated directly — use one of the concrete types.

## anyOf

{{#composedSchemas}}
{{#anyOf}}
- {{#complexType}}[**{{{dataType}}}**]({{complexType}}.md){{/complexType}}{{^complexType}}**{{{dataType}}}**{{/complexType}}
{{/anyOf}}
{{/composedSchemas}}
{{#discriminator}}

The concrete type is selected by the `{{propertyName}}` discriminator property.
{{/discriminator}}
{{/anyOf.isEmpty}}
{{/oneOf.isEmpty}}
{{^oneOf.isEmpty}}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ class {{classname}}Test extends TestCase
// TODO: implement
self::markTestIncomplete('Not implemented');
}
{{! Composed (oneOf/anyOf) models are dispatchers with no own properties: their `vars` are the }}
{{! members' flattened properties, so per-property test stubs would be meaningless. Skip them. }}
{{^oneOf}}
{{^anyOf}}
{{#vars}}

/**
Expand All @@ -81,6 +85,8 @@ class {{classname}}Test extends TestCase
self::markTestIncomplete('Not implemented');
}
{{/vars}}
{{/anyOf}}
{{/oneOf}}
}
{{/model}}
{{/models}}
Loading
Loading