Skip to content

Commit 95b2f06

Browse files
better type detection in serialized parameters
- OAI/OpenAPI-Specification#4743 recommends also looking inside "allOf" - if a "$ref" was seen, adjacent keywords are no longer ignored
1 parent 7742401 commit 95b2f06

File tree

3 files changed

+73
-18
lines changed

3 files changed

+73
-18
lines changed

Changes

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
Revision history for OpenAPI-Modern
22

33
{{$NEXT}}
4+
- better type inference for serialized parameters (also looks inside
5+
the "allOf" keyword, and respects keywords adjacent to "$ref")
46

57
0.087 2025-06-17 21:01:06Z
68
- various fixes to instance and keyword locations in some errors

lib/OpenAPI/Modern.pm

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -720,8 +720,8 @@ sub _validate_path_parameter ($self, $state, $param_obj, $path_captures) {
720720
return E({ %$state, keyword => 'style' }, 'only style: simple is supported in path parameters')
721721
if ($param_obj->{style}//'simple') ne 'simple';
722722

723-
my $types = $self->_type_in_schema($param_obj->{schema}, { %$state, schema_path => $state->{schema_path}.'/schema' });
724-
if (grep $_ eq 'array', @$types or grep $_ eq 'object', @$types) {
723+
my @types = $self->_type_in_schema($param_obj->{schema}, { %$state, schema_path => $state->{schema_path}.'/schema' });
724+
if (grep $_ eq 'array', @types or grep $_ eq 'object', @types) {
725725
return E($state, 'deserializing to non-primitive types is not yet supported in path parameters');
726726
}
727727

@@ -761,8 +761,8 @@ sub _validate_query_parameter ($self, $state, $param_obj, $uri) {
761761
return E({ %$state, keyword => 'style' }, 'only style: form is supported in query parameters')
762762
if ($param_obj->{style}//'form') ne 'form';
763763

764-
my $types = $self->_type_in_schema($param_obj->{schema}, { %$state, schema_path => $state->{schema_path}.'/schema' });
765-
if (grep $_ eq 'array', @$types or grep $_ eq 'object', @$types) {
764+
my @types = $self->_type_in_schema($param_obj->{schema}, { %$state, schema_path => $state->{schema_path}.'/schema' });
765+
if (grep $_ eq 'array', @types or grep $_ eq 'object', @types) {
766766
return E($state, 'deserializing to non-primitive types is not yet supported in query parameters');
767767
}
768768

@@ -791,19 +791,19 @@ sub _validate_header_parameter ($self, $state, $header_name, $header_obj, $heade
791791
# line value from a field line."
792792
my @values = map s/^\s*//r =~ s/\s*$//r, map split(/,/, $_), $headers->every_header($header_name)->@*;
793793

794-
my $types = $self->_type_in_schema($header_obj->{schema}, { %$state, schema_path => $state->{schema_path}.'/schema' });
794+
my @types = $self->_type_in_schema($header_obj->{schema}, { %$state, schema_path => $state->{schema_path}.'/schema' });
795795

796796
# RFC9110 §5.3-1: "A recipient MAY combine multiple field lines within a field section that have
797797
# the same field name into one field line, without changing the semantics of the message, by
798798
# appending each subsequent field line value to the initial field line value in order, separated
799799
# by a comma (",") and optional whitespace (OWS, defined in Section 5.6.3). For consistency, use
800800
# comma SP."
801801
my $data;
802-
if (grep $_ eq 'array', @$types) {
802+
if (grep $_ eq 'array', @types) {
803803
# style=simple, explode=false or true: "blue,black,brown" -> ["blue","black","brown"]
804804
$data = \@values;
805805
}
806-
elsif (grep $_ eq 'object', @$types) {
806+
elsif (grep $_ eq 'object', @types) {
807807
if ($header_obj->{explode}//false) {
808808
# style=simple, explode=true: "R=100,G=200,B=150" -> { "R": 100, "G": 200, "B": 150 }
809809
$data = +{ map m/^([^=]*)=?(.*)$/g, @values };
@@ -949,17 +949,25 @@ sub _resolve_ref ($self, $entity_type, $ref, $state) {
949949
return $schema_info->{schema};
950950
}
951951

952-
# determines the type(s) requested in a schema, and the new schema.
952+
# determines the type(s) expected in a schema
953953
sub _type_in_schema ($self, $schema, $state) {
954954
return [] if not is_plain_hashref($schema);
955955

956-
while (my $ref = $schema->{'$ref'}) {
956+
my @types;
957+
958+
push @types, is_plain_arrayref($schema->{type}) ? ($schema->{types}->@*) : ($schema->{type})
959+
if exists $schema->{type};
960+
961+
push @types, map $self->_type_in_schema($schema->{allOf}[$_],
962+
{ %$state, schema_path => $state->{schema_path}.'/allOf/'.$_ }), 0..$schema->{allOf}->$#*
963+
if exists $schema->{allOf};
964+
965+
if (my $ref = $schema->{'$ref'}) {
957966
$schema = $self->_resolve_ref('schema', $ref, $state);
967+
push @types, $self->_type_in_schema($schema, $state);
958968
}
959-
my $types = is_plain_hashref($schema) ? $schema->{type}//[] : [];
960-
$types = [ $types ] if not is_plain_arrayref($types);
961969

962-
return $types;
970+
return @types;
963971
}
964972

965973
# evaluates data against the subschema at the current state ___location

t/validate_request.t

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2002,14 +2002,27 @@ paths:
20022002
schema:
20032003
$ref: '#/paths/~1foo/get/parameters/2/schema'
20042004
not: true
2005-
- name: ArrayWithBrokenRef
2006-
in: header
2007-
schema:
2008-
$ref: '#/components/schemas/i_do_not_exist'
20092005
- name: MultipleValuesAsRawString
20102006
in: header
20112007
schema:
20122008
const: 'one , two , three'
2009+
- name: ArrayWithLocalTypeAndRef
2010+
in: header
2011+
schema:
2012+
type: array # if detected, this will be used first to determine the parsing
2013+
$ref: '#/paths/~1foo/get/parameters/3/schema' # this provides type: object
2014+
not: true
2015+
- name: ArrayWithAllOfAndRef
2016+
in: header
2017+
schema:
2018+
allOf:
2019+
- $ref: '#/paths/~1foo/get/parameters/2/schema'
2020+
- not: true
2021+
# must be evaluated last, as broken $refs abort all validation
2022+
- name: ArrayWithBrokenRef
2023+
in: header
2024+
schema:
2025+
$ref: '#/components/schemas/i_do_not_exist'
20132026
YAML
20142027

20152028
my $request = request('GET', 'http://example.com/foo', [ SingleValue => ' mystring ' ]);
@@ -2085,6 +2098,8 @@ YAML
20852098
$request = request('GET', 'http://example.com/foo', [
20862099
ArrayWithRef => 'one, one, three',
20872100
ArrayWithRefAndOtherKeywords => 'one, one, three',
2101+
ArrayWithLocalTypeAndRef => 'one, two, two',
2102+
ArrayWithAllOfAndRef => 'one, three, three',
20882103
ArrayWithBrokenRef => 'hi',
20892104
]);
20902105
cmp_result(
@@ -2110,10 +2125,40 @@ YAML
21102125
absoluteKeywordLocation => $doc_uri->clone->fragment(jsonp(qw(/paths /foo get parameters 6 schema not)))->to_string,
21112126
error => 'subschema is true',
21122127
},
2128+
{
2129+
instanceLocation => '/request/header/ArrayWithLocalTypeAndRef',
2130+
keywordLocation => jsonp(qw(/paths /foo get parameters 8 schema $ref type)),
2131+
absoluteKeywordLocation => $doc_uri->clone->fragment(jsonp(qw(/paths /foo get parameters 3 schema type)))->to_string,
2132+
error => 'got array, not object',
2133+
},
2134+
{
2135+
instanceLocation => '/request/header/ArrayWithLocalTypeAndRef',
2136+
keywordLocation => jsonp(qw(/paths /foo get parameters 8 schema not)),
2137+
absoluteKeywordLocation => $doc_uri->clone->fragment(jsonp(qw(/paths /foo get parameters 8 schema not)))->to_string,
2138+
error => 'subschema is true',
2139+
},
2140+
{
2141+
instanceLocation => '/request/header/ArrayWithAllOfAndRef',
2142+
keywordLocation => jsonp(qw(/paths /foo get parameters 9 schema allOf 0 $ref uniqueItems)),
2143+
absoluteKeywordLocation => $doc_uri->clone->fragment(jsonp(qw(/paths /foo get parameters 2 schema uniqueItems)))->to_string,
2144+
error => 'items at indices 1 and 2 are not unique',
2145+
},
2146+
{
2147+
instanceLocation => '/request/header/ArrayWithAllOfAndRef',
2148+
keywordLocation => jsonp(qw(/paths /foo get parameters 9 schema allOf 1 not)),
2149+
absoluteKeywordLocation => $doc_uri->clone->fragment(jsonp(qw(/paths /foo get parameters 9 schema allOf 1 not)))->to_string,
2150+
error => 'subschema is true',
2151+
},
2152+
{
2153+
instanceLocation => '/request/header/ArrayWithAllOfAndRef',
2154+
keywordLocation => jsonp(qw(/paths /foo get parameters 9 schema allOf)),
2155+
absoluteKeywordLocation => $doc_uri->clone->fragment(jsonp(qw(/paths /foo get parameters 9 schema allOf)))->to_string,
2156+
error => 'subschemas 0, 1 are not valid',
2157+
},
21132158
{
21142159
instanceLocation => '/request/header/ArrayWithBrokenRef',
2115-
keywordLocation => jsonp(qw(/paths /foo get parameters 7 schema $ref)),
2116-
absoluteKeywordLocation => $doc_uri->clone->fragment(jsonp(qw(/paths /foo get parameters 7 schema $ref)))->to_string,
2160+
keywordLocation => jsonp(qw(/paths /foo get parameters 10 schema $ref)),
2161+
absoluteKeywordLocation => $doc_uri->clone->fragment(jsonp(qw(/paths /foo get parameters 10 schema $ref)))->to_string,
21172162
error => 'EXCEPTION: unable to find resource "'.$doc_uri.'#/components/schemas/i_do_not_exist"',
21182163
},
21192164
],

0 commit comments

Comments
 (0)