Skip to content

Map array<K, V> to type: object + additionalProperties#2003

Merged
DerManoMann merged 1 commit intozircote:masterfrom
krissss:feat/2001-map-type-to-object
Apr 28, 2026
Merged

Map array<K, V> to type: object + additionalProperties#2003
DerManoMann merged 1 commit intozircote:masterfrom
krissss:feat/2001-map-type-to-object

Conversation

@krissss
Copy link
Copy Markdown
Contributor

@krissss krissss commented Apr 23, 2026

Closes #2001

Summary

  • TypeInfoTypeResolver now maps array<K, V> (with explicit key type) to type: object + additionalProperties instead of type: array + items
  • array<T>, T[], list<T> (default int|string key) keep existing type: array + items behaviour
  • ArrayShapeType (e.g. array{foo:bool}) also correctly resolves to type: object + additionalProperties
  • LegacyTypeResolver is not affected — it continues to produce type: array + items for all array types

Test plan

  • TypeResolverTest — 152 tests pass (both 3.0.0 and 3.1.0, both legacy and type-info resolvers)
  • AugmentPropertiesTest — map assertion merged into testTypedProperties
  • Nested array<int, T> in union types correctly produces object + additionalProperties

…nfoTypeResolver (zircote#2001)

Previously all PHP array types were mapped to `type: array` + `items`.
Now `array<K, V>` with an explicit key type resolves to `type: object`
with `additionalProperties`, while `array<T>`, `T[]` and `list<T>` keep
their existing `type: array` + `items` behaviour.
Copy link
Copy Markdown
Collaborator

@DerManoMann DerManoMann left a comment

Choose a reason for hiding this comment

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

Looks like it is doing what it should. I do not quite like the UnionType check, but there might be no alternative.

Only think to consider, even thought there seems to be a bit of variation, is wheter there is now a lot of duplication that could be simplified around the creation of nested Schema.

$this->setSchemaType($schema->items, $type->getCollectionValueType(), $analysis);
$this->type2ref($schema->items, $analysis);
}
if ($type->isList() || $type->getCollectionKeyType() instanceof UnionType) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is tricky. UnionType means int|string so could be an object, we just don't know. However, from what I understand this happens when no key is specified.
I have to admin I do not quite like this check as it is quite loose, however I do not know if there is a better alternative... hmmm.

@DerManoMann
Copy link
Copy Markdown
Collaborator

Tried to come up with an example that might conflict with the union check over the weekend...
How about this:

enum TypeEnum: string
{
    case First = 'first';
    case Second = 'second';
}

#[OAT\Schema()]
class TypedProperties
{
    /**
     * A map of stringy to string.
     *
     * @var \ArrayObject<TypeEnum|string,string> $stringyMap
     */
    #[OAT\Property]
    public \ArrayObject $stringyMap;
}

@DerManoMann
Copy link
Copy Markdown
Collaborator

Claude suggests to check if all union types are built in types, but that still feels ambiguous to me. What about checking if the union contains int?

Casting rules around array keys are tricky so maybe requiring to be explicit about int is a good thing?

@krissss
Copy link
Copy Markdown
Contributor Author

krissss commented Apr 28, 2026

Thanks for the detailed feedback and the edge case example!

Regarding the array<int, string> case:

In PHP, json_encode only produces a JSON array when keys are sequential starting from 0. For non-sequential int keys like [1 => 'a', 3 => 'b'], the result is a JSON object {"1":"a","3":"b"}.

The key insight is: if someone writes array<int, string>, they are explicitly declaring a key type — which means they intend a map. If they wanted a sequential list, they would simply write array<string>, string[], or list<string>. The explicit key type is what distinguishes the two.

Regarding the ArrayObject<TypeEnum|string, string> example:

PHP array keys only allow int or string, so TypeEnum would be cast to its string backing value at runtime. While the PHPDoc annotation can declare such a key type, this pattern is extremely uncommon in practice. The vast majority of real-world cases are array<string, V> and array<int, V>.

Regarding the "check if key contains int" suggestion:

This would cause array<int, string> to be treated as type: array, but as discussed above, anyone writing array<int, string> most likely intends a map, not a list.

@DerManoMann
Copy link
Copy Markdown
Collaborator

Yeah, I agree this is a bit of a mess. Also agree that my example is a bit far fetched ;)

Let's go with this for now. We can always refine this if needed.

Copy link
Copy Markdown
Collaborator

@DerManoMann DerManoMann left a comment

Choose a reason for hiding this comment

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

Thanks @krissss

@DerManoMann DerManoMann merged commit f66289a into zircote:master Apr 28, 2026
17 checks passed
@github-actions
Copy link
Copy Markdown
Contributor

Created backport PR for 5.x:

Please cherry-pick the changes locally and resolve any conflicts.

git fetch origin backport-2003-to-5.x
git worktree add --checkout .worktree/backport-2003-to-5.x backport-2003-to-5.x
cd .worktree/backport-2003-to-5.x
git reset --hard HEAD^
git cherry-pick -x f66289ab9c9c3a1cf70222e0bebbe7c6c7109f2f

@krissss krissss deleted the feat/2001-map-type-to-object branch April 29, 2026 01:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Support array<K, V> mapping to type: object + additionalProperties

2 participants