Skip to content

Commit b2f622c

Browse files
authored
Fix map and free form object detection issue in 3.1 spec (#17624)
* fix map issue in 3.1 spec * fix, add tests * update samples * update * manully fix spec * revert * fix rust model
1 parent c2ec0ba commit b2f622c

File tree

8 files changed

+118
-12
lines changed

8 files changed

+118
-12
lines changed

modules/openapi-generator/src/main/java/org/openapitools/codegen/IJsonSchemaValidationProperties.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,8 @@ default void setTypeProperties(Schema p) {
329329
setIsFreeFormObject(true);
330330
// TODO: remove below later after updating generators to properly use isFreeFormObject
331331
setIsMap(true);
332+
} else if (ModelUtils.isMapSchema(p)) {
333+
setIsMap(true);
332334
} else if (ModelUtils.isTypeObjectSchema(p)) {
333335
setIsMap(true);
334336
}

modules/openapi-generator/src/main/java/org/openapitools/codegen/utils/ModelUtils.java

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,16 @@ public static String getSimpleRef(String ref) {
417417
* @return true if the specified schema is an Object schema.
418418
*/
419419
public static boolean isTypeObjectSchema(Schema schema) {
420-
return SchemaTypeUtil.OBJECT_TYPE.equals(schema.getType());
420+
if (schema instanceof JsonSchema) { // 3.1 spec
421+
if (schema.getTypes() != null && schema.getTypes().size() == 1) {
422+
return SchemaTypeUtil.OBJECT_TYPE.equals(schema.getTypes().iterator().next());
423+
} else {
424+
// null type or multiple types, e.g. [string, integer]
425+
return false;
426+
}
427+
} else { // 3.0.x or 2.0 spec
428+
return SchemaTypeUtil.OBJECT_TYPE.equals(schema.getType());
429+
}
421430
}
422431

423432
/**
@@ -567,9 +576,14 @@ public static boolean isMapSchema(Schema schema) {
567576
return false;
568577
}
569578

570-
return (schema instanceof MapSchema) ||
571-
(schema.getAdditionalProperties() instanceof Schema) ||
572-
(schema.getAdditionalProperties() instanceof Boolean && (Boolean) schema.getAdditionalProperties());
579+
if (schema instanceof JsonSchema) { // 3.1 spec
580+
return ((schema.getAdditionalProperties() instanceof JsonSchema) ||
581+
(schema.getAdditionalProperties() instanceof Boolean && (Boolean) schema.getAdditionalProperties()));
582+
} else { // 3.0 or 2.x spec
583+
return (schema instanceof MapSchema) ||
584+
(schema.getAdditionalProperties() instanceof Schema) ||
585+
(schema.getAdditionalProperties() instanceof Boolean && (Boolean) schema.getAdditionalProperties());
586+
}
573587
}
574588

575589
/**
@@ -790,6 +804,31 @@ public static boolean isFreeFormObject(Schema schema) {
790804
return false;
791805
}
792806

807+
if (schema instanceof JsonSchema) { // 3.1 spec
808+
if (isComposedSchema(schema)) { // composed schema, e.g. allOf, oneOf, anyOf
809+
return false;
810+
}
811+
812+
if (schema.getProperties() != null && !schema.getProperties().isEmpty()) { // has properties
813+
return false;
814+
}
815+
816+
if (schema.getAdditionalProperties() instanceof Boolean && (Boolean) schema.getAdditionalProperties()) {
817+
return true;
818+
} else if (schema.getAdditionalProperties() instanceof JsonSchema) {
819+
return true;
820+
} else if (schema.getTypes() != null) {
821+
if (schema.getTypes().size() == 1) { // types = [object]
822+
return SchemaTypeUtil.OBJECT_TYPE.equals(schema.getTypes().iterator().next());
823+
} else { // has more than 1 type, e.g. types = [integer, string]
824+
return false;
825+
}
826+
}
827+
828+
return false;
829+
}
830+
831+
// 3.0.x spec or 2.x spec
793832
// not free-form if allOf, anyOf, oneOf is not empty
794833
if (isComposedSchema(schema)) {
795834
List<Schema> interfaces = ModelUtils.getInterfaces(schema);

modules/openapi-generator/src/main/resources/rust/model.mustache

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ impl {{{classname}}} {
8282
pub fn new({{#requiredVars}}{{{name}}}: {{#isNullable}}Option<{{/isNullable}}{{#isEnum}}{{#isArray}}{{#uniqueItems}}std::collections::HashSet<{{/uniqueItems}}{{^uniqueItems}}Vec<{{/uniqueItems}}{{/isArray}}{{{enumName}}}{{#isArray}}>{{/isArray}}{{/isEnum}}{{^isEnum}}{{{dataType}}}{{/isEnum}}{{#isNullable}}>{{/isNullable}}{{^-last}}, {{/-last}}{{/requiredVars}}) -> {{{classname}}} {
8383
{{{classname}}} {
8484
{{#vars}}
85-
{{{name}}}{{^required}}{{#isContainer}}{{#isArray}}: None{{/isArray}}{{#isMap}}: None{{/isMap}}{{^isArray}}{{^isMap}}{{#isNullable}}: None{{/isNullable}}{{/isMap}}{{/isArray}}{{/isContainer}}{{^isContainer}}: None{{/isContainer}}{{/required}}{{#required}}{{#isModel}}: {{^isNullable}}Box::new({{{name}}}){{/isNullable}}{{#isNullable}}if let Some(x) = {{{name}}} {Some(Box::new(x))} else {None}{{/isNullable}}{{/isModel}}{{/required}},
85+
{{{name}}}{{^required}}{{#isFreeFormObject}}: None{{/isFreeFormObject}}{{#isContainer}}{{#isArray}}: None{{/isArray}}{{#isMap}}: None{{/isMap}}{{^isArray}}{{^isMap}}{{#isNullable}}: None{{/isNullable}}{{/isMap}}{{/isArray}}{{/isContainer}}{{^isContainer}}: None{{/isContainer}}{{/required}}{{#required}}{{#isModel}}: {{^isNullable}}Box::new({{{name}}}){{/isNullable}}{{#isNullable}}if let Some(x) = {{{name}}} {Some(Box::new(x))} else {None}{{/isNullable}}{{/isModel}}{{/required}},
8686
{{/vars}}
8787
}
8888
}

modules/openapi-generator/src/test/java/org/openapitools/codegen/utils/ModelUtilsTest.java

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,4 +294,19 @@ public void testSimpleRefDecoding() {
294294
String decoded = ModelUtils.getSimpleRef("#/components/~01%20Hallo~1Welt");
295295
Assert.assertEquals(decoded, "~1 Hallo/Welt");
296296
}
297+
298+
// 3.1 spec test
299+
@Test
300+
public void testIsMapSchema() {
301+
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/3_1/schema.yaml");
302+
Schema misc = ModelUtils.getSchema(openAPI, "Misc");
303+
304+
// test map
305+
Assert.assertTrue(ModelUtils.isMapSchema((Schema) misc.getProperties().get("map1")));
306+
307+
// test free form object
308+
Assert.assertTrue(ModelUtils.isFreeFormObject((Schema) misc.getProperties().get("free_form_object_1")));
309+
Assert.assertTrue(ModelUtils.isFreeFormObject((Schema) misc.getProperties().get("free_form_object_2")));
310+
Assert.assertTrue(ModelUtils.isFreeFormObject((Schema) misc.getProperties().get("free_form_object_3")));
311+
}
297312
}

modules/openapi-generator/src/test/resources/3_1/regression-16119.yaml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ openapi: 3.1.0
33
info:
44
title: double-option-hashmap
55
version: 0.1.0
6-
license:
7-
name: MIT
8-
identifier: MIT
96
servers:
107
- url: http://api.example.xyz/v1
118
paths:
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
openapi: 3.1.0
2+
servers:
3+
- url: 'http://petstore.swagger.io/v2'
4+
info:
5+
description: >-
6+
This is a sample server Petstore server. For this sample, you can use the api key
7+
`special-key` to test the authorization filters.
8+
version: 1.0.0
9+
title: OpenAPI Petstore
10+
license:
11+
name: Apache-2.0
12+
url: 'https://www.apache.org/licenses/LICENSE-2.0.html'
13+
tags:
14+
- name: pet
15+
description: Everything about your Pets
16+
- name: store
17+
description: Access to Petstore orders
18+
- name: user
19+
description: Operations about user
20+
paths:
21+
/dummy:
22+
post:
23+
tags:
24+
- dummy
25+
summary: dummy operation
26+
description: ''
27+
operationId: dummy_operation
28+
responses:
29+
'200':
30+
description: successful operation
31+
'405':
32+
description: Invalid input
33+
externalDocs:
34+
description: Find out more about Swagger
35+
url: 'http://swagger.io'
36+
components:
37+
schemas:
38+
Misc:
39+
description: Schema tests
40+
type: object
41+
properties:
42+
free_form_object_1:
43+
type: object
44+
free_form_object_2:
45+
type: object
46+
additionalProperties: true
47+
free_form_object_3:
48+
type: object
49+
additionalProperties: {}
50+
map1:
51+
type: object
52+
additionalProperties:
53+
type: string

samples/client/others/rust/reqwest-regression-16119/Cargo.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@ name = "regression-16119-reqwest"
33
version = "0.1.0"
44
authors = ["OpenAPI Generator team and contributors"]
55
description = "No description provided (generated by Openapi Generator https://github.com/openapitools/openapi-generator)"
6-
license = "MIT"
6+
# Override this license by providing a License Object in the OpenAPI.
7+
license = "Unlicense"
78
edition = "2018"
89

910
[dependencies]
1011
serde = "^1.0"
1112
serde_derive = "^1.0"
12-
serde_with = "^2.0"
1313
serde_json = "^1.0"
1414
url = "^2.2"
1515
uuid = { version = "^1.0", features = ["serde", "v4"] }

samples/client/others/rust/reqwest-regression-16119/src/models/parent.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@
1313

1414
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1515
pub struct Parent {
16-
#[serde(rename = "child", default, with = "::serde_with::rust::double_option", skip_serializing_if = "Option::is_none")]
17-
pub child: Option<Option<::std::collections::HashMap<String, serde_json::Value>>>,
16+
#[serde(rename = "child", skip_serializing_if = "Option::is_none")]
17+
pub child: Option<::std::collections::HashMap<String, serde_json::Value>>,
1818
}
1919

2020
impl Parent {

0 commit comments

Comments
 (0)