From d4d519690781c35b4e65cdf344fb28ecaf203e01 Mon Sep 17 00:00:00 2001 From: William Cheng Date: Mon, 11 Dec 2023 17:13:55 +0800 Subject: [PATCH] Better handling of any type in v3.1 spec (#17370) * fix NPE in the example generator * fix any type in 3.1 spec * use log error instead --- .../codegen/OpenAPINormalizer.java | 12 +- .../codegen/examples/ExampleGenerator.java | 4 + .../src/test/resources/3_1/java/petstore.yaml | 3 + .../okhttp-gson-3.1/.openapi-generator/FILES | 2 + .../petstore/java/okhttp-gson-3.1/README.md | 1 + .../java/okhttp-gson-3.1/api/openapi.yaml | 3 + .../java/okhttp-gson-3.1/docs/AnyTypeTest.md | 13 + .../java/org/openapitools/client/JSON.java | 1 + .../client/model/AnyTypeTest.java | 291 ++++++++++++++++++ .../openapitools/client/api/PetApiTest.java | 7 + .../client/model/AnyTypeTestTest.java | 49 +++ 11 files changed, 385 insertions(+), 1 deletion(-) create mode 100644 samples/client/petstore/java/okhttp-gson-3.1/docs/AnyTypeTest.md create mode 100644 samples/client/petstore/java/okhttp-gson-3.1/src/main/java/org/openapitools/client/model/AnyTypeTest.java create mode 100644 samples/client/petstore/java/okhttp-gson-3.1/src/test/java/org/openapitools/client/model/AnyTypeTestTest.java diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java index 789b9c9f95c..3f5a39baf9c 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java @@ -911,10 +911,20 @@ public class OpenAPINormalizer { return schema; } - if (schema == null || schema.getTypes() == null) { + if (schema == null) { return null; } + if (schema instanceof JsonSchema && + schema.get$schema() == null && + schema.getTypes() == null && schema.getType() == null) { + // convert any type in v3.1 to empty schema (any type in v3.0 spec), any type example: + // components: + // schemas: + // any_type: {} + return new Schema(); + } + // process null if (schema.getTypes().contains("null")) { schema.setNullable(true); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/examples/ExampleGenerator.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/examples/ExampleGenerator.java index ac14f2b9e6b..a1c16819fb0 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/examples/ExampleGenerator.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/examples/ExampleGenerator.java @@ -224,6 +224,10 @@ public class ExampleGenerator { } private Object resolvePropertyToExample(String propertyName, String mediaType, Schema property, Set processedModels) { + if (property == null) { + LOGGER.error("Property schema shouldn't be null. Please report the issue to the openapi-generator team."); + return ""; + } LOGGER.debug("Resolving example for property {}...", property); if (property.getExample() != null) { LOGGER.debug("Example set in openapi spec, returning example: '{}'", property.getExample().toString()); diff --git a/modules/openapi-generator/src/test/resources/3_1/java/petstore.yaml b/modules/openapi-generator/src/test/resources/3_1/java/petstore.yaml index 8436db26380..a8378849d12 100644 --- a/modules/openapi-generator/src/test/resources/3_1/java/petstore.yaml +++ b/modules/openapi-generator/src/test/resources/3_1/java/petstore.yaml @@ -819,3 +819,6 @@ components: default: red simple_text: type: string + any_type_test: + properties: + any_type_property: {} diff --git a/samples/client/petstore/java/okhttp-gson-3.1/.openapi-generator/FILES b/samples/client/petstore/java/okhttp-gson-3.1/.openapi-generator/FILES index a90799f32aa..0a065d67dc8 100644 --- a/samples/client/petstore/java/okhttp-gson-3.1/.openapi-generator/FILES +++ b/samples/client/petstore/java/okhttp-gson-3.1/.openapi-generator/FILES @@ -6,6 +6,7 @@ api/openapi.yaml build.gradle build.sbt docs/Animal.md +docs/AnyTypeTest.md docs/Cat.md docs/Category.md docs/Dog.md @@ -56,6 +57,7 @@ src/main/java/org/openapitools/client/auth/OAuthOkHttpClient.java src/main/java/org/openapitools/client/auth/RetryingOAuth.java src/main/java/org/openapitools/client/model/AbstractOpenApiSchema.java src/main/java/org/openapitools/client/model/Animal.java +src/main/java/org/openapitools/client/model/AnyTypeTest.java src/main/java/org/openapitools/client/model/Cat.java src/main/java/org/openapitools/client/model/Category.java src/main/java/org/openapitools/client/model/Dog.java diff --git a/samples/client/petstore/java/okhttp-gson-3.1/README.md b/samples/client/petstore/java/okhttp-gson-3.1/README.md index c285ab1f2b6..f79b4cc2c79 100644 --- a/samples/client/petstore/java/okhttp-gson-3.1/README.md +++ b/samples/client/petstore/java/okhttp-gson-3.1/README.md @@ -140,6 +140,7 @@ Class | Method | HTTP request | Description ## Documentation for Models - [Animal](docs/Animal.md) + - [AnyTypeTest](docs/AnyTypeTest.md) - [Cat](docs/Cat.md) - [Category](docs/Category.md) - [Dog](docs/Dog.md) diff --git a/samples/client/petstore/java/okhttp-gson-3.1/api/openapi.yaml b/samples/client/petstore/java/okhttp-gson-3.1/api/openapi.yaml index 9a17e6da6e4..0b8fd95ef7d 100644 --- a/samples/client/petstore/java/okhttp-gson-3.1/api/openapi.yaml +++ b/samples/client/petstore/java/okhttp-gson-3.1/api/openapi.yaml @@ -879,6 +879,9 @@ components: - className simple_text: type: string + any_type_test: + properties: + any_type_property: {} updatePetWithForm_request: properties: name: diff --git a/samples/client/petstore/java/okhttp-gson-3.1/docs/AnyTypeTest.md b/samples/client/petstore/java/okhttp-gson-3.1/docs/AnyTypeTest.md new file mode 100644 index 00000000000..40dd456234a --- /dev/null +++ b/samples/client/petstore/java/okhttp-gson-3.1/docs/AnyTypeTest.md @@ -0,0 +1,13 @@ + + +# AnyTypeTest + + +## Properties + +| Name | Type | Description | Notes | +|------------ | ------------- | ------------- | -------------| +|**anyTypeProperty** | **Object** | | [optional] | + + + diff --git a/samples/client/petstore/java/okhttp-gson-3.1/src/main/java/org/openapitools/client/JSON.java b/samples/client/petstore/java/okhttp-gson-3.1/src/main/java/org/openapitools/client/JSON.java index 6791d767d97..842131f6c78 100644 --- a/samples/client/petstore/java/okhttp-gson-3.1/src/main/java/org/openapitools/client/JSON.java +++ b/samples/client/petstore/java/okhttp-gson-3.1/src/main/java/org/openapitools/client/JSON.java @@ -122,6 +122,7 @@ public class JSON { gsonBuilder.registerTypeAdapter(OffsetDateTime.class, offsetDateTimeTypeAdapter); gsonBuilder.registerTypeAdapter(LocalDate.class, localDateTypeAdapter); gsonBuilder.registerTypeAdapter(byte[].class, byteArrayAdapter); + gsonBuilder.registerTypeAdapterFactory(new org.openapitools.client.model.AnyTypeTest.CustomTypeAdapterFactory()); gsonBuilder.registerTypeAdapterFactory(new org.openapitools.client.model.Cat.CustomTypeAdapterFactory()); gsonBuilder.registerTypeAdapterFactory(new org.openapitools.client.model.Category.CustomTypeAdapterFactory()); gsonBuilder.registerTypeAdapterFactory(new org.openapitools.client.model.Dog.CustomTypeAdapterFactory()); diff --git a/samples/client/petstore/java/okhttp-gson-3.1/src/main/java/org/openapitools/client/model/AnyTypeTest.java b/samples/client/petstore/java/okhttp-gson-3.1/src/main/java/org/openapitools/client/model/AnyTypeTest.java new file mode 100644 index 00000000000..e00b14ba231 --- /dev/null +++ b/samples/client/petstore/java/okhttp-gson-3.1/src/main/java/org/openapitools/client/model/AnyTypeTest.java @@ -0,0 +1,291 @@ +/* + * OpenAPI Petstore + * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +package org.openapitools.client.model; + +import java.util.Objects; +import com.google.gson.TypeAdapter; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.annotations.SerializedName; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.util.Arrays; +import org.openapitools.jackson.nullable.JsonNullable; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; +import com.google.gson.TypeAdapterFactory; +import com.google.gson.reflect.TypeToken; +import com.google.gson.TypeAdapter; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; + +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import org.openapitools.client.JSON; + +/** + * AnyTypeTest + */ +@javax.annotation.Generated(value = "org.openapitools.codegen.languages.JavaClientCodegen") +public class AnyTypeTest { + public static final String SERIALIZED_NAME_ANY_TYPE_PROPERTY = "any_type_property"; + @SerializedName(SERIALIZED_NAME_ANY_TYPE_PROPERTY) + private Object anyTypeProperty = null; + + public AnyTypeTest() { + } + + public AnyTypeTest anyTypeProperty(Object anyTypeProperty) { + this.anyTypeProperty = anyTypeProperty; + return this; + } + + /** + * Get anyTypeProperty + * @return anyTypeProperty + **/ + @javax.annotation.Nullable + public Object getAnyTypeProperty() { + return anyTypeProperty; + } + + public void setAnyTypeProperty(Object anyTypeProperty) { + this.anyTypeProperty = anyTypeProperty; + } + + /** + * A container for additional, undeclared properties. + * This is a holder for any undeclared properties as specified with + * the 'additionalProperties' keyword in the OAS document. + */ + private Map additionalProperties; + + /** + * Set the additional (undeclared) property with the specified name and value. + * If the property does not already exist, create it otherwise replace it. + * + * @param key name of the property + * @param value value of the property + * @return the AnyTypeTest instance itself + */ + public AnyTypeTest putAdditionalProperty(String key, Object value) { + if (this.additionalProperties == null) { + this.additionalProperties = new HashMap(); + } + this.additionalProperties.put(key, value); + return this; + } + + /** + * Return the additional (undeclared) property. + * + * @return a map of objects + */ + public Map getAdditionalProperties() { + return additionalProperties; + } + + /** + * Return the additional (undeclared) property with the specified name. + * + * @param key name of the property + * @return an object + */ + public Object getAdditionalProperty(String key) { + if (this.additionalProperties == null) { + return null; + } + return this.additionalProperties.get(key); + } + + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + AnyTypeTest anyTypeTest = (AnyTypeTest) o; + return Objects.equals(this.anyTypeProperty, anyTypeTest.anyTypeProperty)&& + Objects.equals(this.additionalProperties, anyTypeTest.additionalProperties); + } + + private static boolean equalsNullable(JsonNullable a, JsonNullable b) { + return a == b || (a != null && b != null && a.isPresent() && b.isPresent() && Objects.deepEquals(a.get(), b.get())); + } + + @Override + public int hashCode() { + return Objects.hash(anyTypeProperty, additionalProperties); + } + + private static int hashCodeNullable(JsonNullable a) { + if (a == null) { + return 1; + } + return a.isPresent() ? Arrays.deepHashCode(new Object[]{a.get()}) : 31; + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class AnyTypeTest {\n"); + sb.append(" anyTypeProperty: ").append(toIndentedString(anyTypeProperty)).append("\n"); + sb.append(" additionalProperties: ").append(toIndentedString(additionalProperties)).append("\n"); + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } + + + public static HashSet openapiFields; + public static HashSet openapiRequiredFields; + + static { + // a set of all properties/fields (JSON key names) + openapiFields = new HashSet(); + openapiFields.add("any_type_property"); + + // a set of required properties/fields (JSON key names) + openapiRequiredFields = new HashSet(); + } + + /** + * Validates the JSON Element and throws an exception if issues found + * + * @param jsonElement JSON Element + * @throws IOException if the JSON Element is invalid with respect to AnyTypeTest + */ + public static void validateJsonElement(JsonElement jsonElement) throws IOException { + if (jsonElement == null) { + if (!AnyTypeTest.openapiRequiredFields.isEmpty()) { // has required fields but JSON element is null + throw new IllegalArgumentException(String.format("The required field(s) %s in AnyTypeTest is not found in the empty JSON string", AnyTypeTest.openapiRequiredFields.toString())); + } + } + JsonObject jsonObj = jsonElement.getAsJsonObject(); + } + + public static class CustomTypeAdapterFactory implements TypeAdapterFactory { + @SuppressWarnings("unchecked") + @Override + public TypeAdapter create(Gson gson, TypeToken type) { + if (!AnyTypeTest.class.isAssignableFrom(type.getRawType())) { + return null; // this class only serializes 'AnyTypeTest' and its subtypes + } + final TypeAdapter elementAdapter = gson.getAdapter(JsonElement.class); + final TypeAdapter thisAdapter + = gson.getDelegateAdapter(this, TypeToken.get(AnyTypeTest.class)); + + return (TypeAdapter) new TypeAdapter() { + @Override + public void write(JsonWriter out, AnyTypeTest value) throws IOException { + JsonObject obj = thisAdapter.toJsonTree(value).getAsJsonObject(); + obj.remove("additionalProperties"); + // serialize additional properties + if (value.getAdditionalProperties() != null) { + for (Map.Entry entry : value.getAdditionalProperties().entrySet()) { + if (entry.getValue() instanceof String) + obj.addProperty(entry.getKey(), (String) entry.getValue()); + else if (entry.getValue() instanceof Number) + obj.addProperty(entry.getKey(), (Number) entry.getValue()); + else if (entry.getValue() instanceof Boolean) + obj.addProperty(entry.getKey(), (Boolean) entry.getValue()); + else if (entry.getValue() instanceof Character) + obj.addProperty(entry.getKey(), (Character) entry.getValue()); + else { + obj.add(entry.getKey(), gson.toJsonTree(entry.getValue()).getAsJsonObject()); + } + } + } + elementAdapter.write(out, obj); + } + + @Override + public AnyTypeTest read(JsonReader in) throws IOException { + JsonElement jsonElement = elementAdapter.read(in); + validateJsonElement(jsonElement); + JsonObject jsonObj = jsonElement.getAsJsonObject(); + // store additional fields in the deserialized instance + AnyTypeTest instance = thisAdapter.fromJsonTree(jsonObj); + for (Map.Entry entry : jsonObj.entrySet()) { + if (!openapiFields.contains(entry.getKey())) { + if (entry.getValue().isJsonPrimitive()) { // primitive type + if (entry.getValue().getAsJsonPrimitive().isString()) + instance.putAdditionalProperty(entry.getKey(), entry.getValue().getAsString()); + else if (entry.getValue().getAsJsonPrimitive().isNumber()) + instance.putAdditionalProperty(entry.getKey(), entry.getValue().getAsNumber()); + else if (entry.getValue().getAsJsonPrimitive().isBoolean()) + instance.putAdditionalProperty(entry.getKey(), entry.getValue().getAsBoolean()); + else + throw new IllegalArgumentException(String.format("The field `%s` has unknown primitive type. Value: %s", entry.getKey(), entry.getValue().toString())); + } else if (entry.getValue().isJsonArray()) { + instance.putAdditionalProperty(entry.getKey(), gson.fromJson(entry.getValue(), List.class)); + } else { // JSON object + instance.putAdditionalProperty(entry.getKey(), gson.fromJson(entry.getValue(), HashMap.class)); + } + } + } + return instance; + } + + }.nullSafe(); + } + } + + /** + * Create an instance of AnyTypeTest given an JSON string + * + * @param jsonString JSON string + * @return An instance of AnyTypeTest + * @throws IOException if the JSON string is invalid with respect to AnyTypeTest + */ + public static AnyTypeTest fromJson(String jsonString) throws IOException { + return JSON.getGson().fromJson(jsonString, AnyTypeTest.class); + } + + /** + * Convert an instance of AnyTypeTest to an JSON string + * + * @return JSON string + */ + public String toJson() { + return JSON.getGson().toJson(this); + } +} + diff --git a/samples/client/petstore/java/okhttp-gson-3.1/src/test/java/org/openapitools/client/api/PetApiTest.java b/samples/client/petstore/java/okhttp-gson-3.1/src/test/java/org/openapitools/client/api/PetApiTest.java index 3983042bcc4..869dc302a97 100644 --- a/samples/client/petstore/java/okhttp-gson-3.1/src/test/java/org/openapitools/client/api/PetApiTest.java +++ b/samples/client/petstore/java/okhttp-gson-3.1/src/test/java/org/openapitools/client/api/PetApiTest.java @@ -373,6 +373,13 @@ public class PetApiTest { assertTrue(pet1.hashCode() == pet1.hashCode()); } + @Test + public void testAnyType() { // test any type in v3.1 spec + AnyTypeTest a = new AnyTypeTest(); + a.setAnyTypeProperty("test"); // shouldn't throw exception + assertEquals("test", a.getAnyTypeProperty()); + } + private Pet createPet() { Pet pet = new Pet(); pet.setId(ThreadLocalRandom.current().nextLong(Long.MAX_VALUE)); diff --git a/samples/client/petstore/java/okhttp-gson-3.1/src/test/java/org/openapitools/client/model/AnyTypeTestTest.java b/samples/client/petstore/java/okhttp-gson-3.1/src/test/java/org/openapitools/client/model/AnyTypeTestTest.java new file mode 100644 index 00000000000..98a31faf01a --- /dev/null +++ b/samples/client/petstore/java/okhttp-gson-3.1/src/test/java/org/openapitools/client/model/AnyTypeTestTest.java @@ -0,0 +1,49 @@ +/* + * OpenAPI Petstore + * This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters. + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +package org.openapitools.client.model; + +import com.google.gson.TypeAdapter; +import com.google.gson.annotations.JsonAdapter; +import com.google.gson.annotations.SerializedName; +import com.google.gson.stream.JsonReader; +import com.google.gson.stream.JsonWriter; +import java.io.IOException; +import java.util.Arrays; +import org.openapitools.jackson.nullable.JsonNullable; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +/** + * Model tests for AnyTypeTest + */ +public class AnyTypeTestTest { + private final AnyTypeTest model = new AnyTypeTest(); + + /** + * Model tests for AnyTypeTest + */ + @Test + public void testAnyTypeTest() { + // TODO: test AnyTypeTest + } + + /** + * Test the property 'anyTypeProperty' + */ + @Test + public void anyTypePropertyTest() { + // TODO: test anyTypeProperty + } + +}