[protobuf-schema] Add flag to handle complex type (#20915)

* Wrap Complex Type

* update

* Add java doc and update test

* change default wrapComplexType to true

* add protobuf-schema-config-complex to CI

* add service proto to address CI failure
This commit is contained in:
lucy66hw 2025-03-19 22:22:40 -07:00 committed by GitHub
parent a2ee3a7cfc
commit 1996d7e8fc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 622 additions and 16 deletions

View File

@ -20,6 +20,7 @@ jobs:
sample: sample:
- 'samples/config/petstore/protobuf-schema/' - 'samples/config/petstore/protobuf-schema/'
- 'samples/config/petstore/protobuf-schema-config/' - 'samples/config/petstore/protobuf-schema-config/'
- 'samples/config/petstore/protobuf-schema-config-complex/'
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Install Protocol Buffers Compiler - name: Install Protocol Buffers Compiler

View File

@ -0,0 +1,9 @@
generatorName: protobuf-schema
outputDir: samples/config/petstore/protobuf-schema-config-complex
inputSpec: modules/openapi-generator/src/test/resources/3_0/protobuf/petstore-complex.yaml
templateDir: modules/openapi-generator/src/main/resources/protobuf-schema
additionalProperties:
packageName: petstore
addJsonNameAnnotation: true
numberedFieldNumberList: true
startEnumsWithUnspecified: true

View File

@ -7,3 +7,4 @@ additionalProperties:
addJsonNameAnnotation: true addJsonNameAnnotation: true
numberedFieldNumberList: true numberedFieldNumberList: true
startEnumsWithUnspecified: true startEnumsWithUnspecified: true
wrapComplexType: false

View File

@ -21,6 +21,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|addJsonNameAnnotation|Append "json_name" annotation to message field when the specification name differs from the protobuf field name| |false| |addJsonNameAnnotation|Append "json_name" annotation to message field when the specification name differs from the protobuf field name| |false|
|numberedFieldNumberList|Field numbers in order.| |false| |numberedFieldNumberList|Field numbers in order.| |false|
|startEnumsWithUnspecified|Introduces "UNSPECIFIED" as the first element of enumerations.| |false| |startEnumsWithUnspecified|Introduces "UNSPECIFIED" as the first element of enumerations.| |false|
|wrapComplexType|Generate Additional message for complex type| |true|
## IMPORT MAPPING ## IMPORT MAPPING

View File

@ -16,6 +16,10 @@
package org.openapitools.codegen.languages; package org.openapitools.codegen.languages;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.media.ArraySchema;
import io.swagger.v3.oas.models.media.MapSchema;
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.media.Schema;
import lombok.Setter; import lombok.Setter;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
@ -58,6 +62,8 @@ public class ProtobufSchemaCodegen extends DefaultCodegen implements CodegenConf
public static final String ADD_JSON_NAME_ANNOTATION = "addJsonNameAnnotation"; public static final String ADD_JSON_NAME_ANNOTATION = "addJsonNameAnnotation";
public static final String WRAP_COMPLEX_TYPE = "wrapComplexType";
private final Logger LOGGER = LoggerFactory.getLogger(ProtobufSchemaCodegen.class); private final Logger LOGGER = LoggerFactory.getLogger(ProtobufSchemaCodegen.class);
@Setter protected String packageName = "openapitools"; @Setter protected String packageName = "openapitools";
@ -68,6 +74,8 @@ public class ProtobufSchemaCodegen extends DefaultCodegen implements CodegenConf
private boolean addJsonNameAnnotation = false; private boolean addJsonNameAnnotation = false;
private boolean wrapComplexType = true;
@Override @Override
public CodegenType getTag() { public CodegenType getTag() {
return CodegenType.SCHEMA; return CodegenType.SCHEMA;
@ -177,6 +185,7 @@ public class ProtobufSchemaCodegen extends DefaultCodegen implements CodegenConf
addSwitch(NUMBERED_FIELD_NUMBER_LIST, "Field numbers in order.", numberedFieldNumberList); addSwitch(NUMBERED_FIELD_NUMBER_LIST, "Field numbers in order.", numberedFieldNumberList);
addSwitch(START_ENUMS_WITH_UNSPECIFIED, "Introduces \"UNSPECIFIED\" as the first element of enumerations.", startEnumsWithUnspecified); addSwitch(START_ENUMS_WITH_UNSPECIFIED, "Introduces \"UNSPECIFIED\" as the first element of enumerations.", startEnumsWithUnspecified);
addSwitch(ADD_JSON_NAME_ANNOTATION, "Append \"json_name\" annotation to message field when the specification name differs from the protobuf field name", addJsonNameAnnotation); addSwitch(ADD_JSON_NAME_ANNOTATION, "Append \"json_name\" annotation to message field when the specification name differs from the protobuf field name", addJsonNameAnnotation);
addSwitch(WRAP_COMPLEX_TYPE, "Generate Additional message for complex type", wrapComplexType);
} }
@Override @Override
@ -215,6 +224,10 @@ public class ProtobufSchemaCodegen extends DefaultCodegen implements CodegenConf
this.addJsonNameAnnotation = convertPropertyToBooleanAndWriteBack(ADD_JSON_NAME_ANNOTATION); this.addJsonNameAnnotation = convertPropertyToBooleanAndWriteBack(ADD_JSON_NAME_ANNOTATION);
} }
if (additionalProperties.containsKey(this.WRAP_COMPLEX_TYPE)) {
this.wrapComplexType = convertPropertyToBooleanAndWriteBack(WRAP_COMPLEX_TYPE);
}
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md")); supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
} }
@ -234,6 +247,224 @@ public class ProtobufSchemaCodegen extends DefaultCodegen implements CodegenConf
return camelize(sanitizeName(operationId)); return camelize(sanitizeName(operationId));
} }
/**
* Creates an array schema from the provided object schema.
*
* @param objectSchema the schema of the object to be wrapped in an array schema
* @return the created array schema
*/
private Schema createArraySchema(Schema objectSchema) {
ArraySchema arraySchema = new ArraySchema();
arraySchema.items(objectSchema);
return arraySchema;
}
/**
* Creates a map schema from the provided object schema.
*
* @param objectSchema the schema of the object to be wrapped in a map schema
* @return the created map schema
*/
private Schema createMapSchema(Schema objectSchema) {
MapSchema mapSchema = new MapSchema();
mapSchema.additionalProperties(objectSchema);
return mapSchema;
}
/**
* Adds a new schema to the OpenAPI components.
*
* @param schema the schema to be added
* @param schemaName the name of the schema
* @param visitedSchema a set of schemas that have already been visited
* @return the reference schema
*/
private Schema addSchemas(Schema schema, String schemaName, Set<Schema> visitedSchema) {
LOGGER.info("Generating new model: {}", schemaName);
ObjectSchema model = new ObjectSchema();
model.setName(schemaName);
Map<String, Schema> properties = new HashMap<>();
properties.put(toVarName(schemaName), schema);
model.setProperties(properties);
Schema refSchema = new Schema();
refSchema.set$ref("#/components/schemas/" + schemaName);
refSchema.setName(schemaName);
visitedSchema.add(refSchema);
openAPI.getComponents().addSchemas(schemaName, model);
return refSchema;
}
/**
* Derive name from schema primitive type
*
* @param schema the schema to derive the name from
* @return the derived name
*/
private String getNameFromSchemaPrimitiveType(Schema schema) {
if (!ModelUtils.isPrimitiveType(schema)) return "";
if(ModelUtils.isNumberSchema(schema)) {
if(schema.getFormat() != null) {
return schema.getFormat();
} else if (typeMapping.get(schema.getType()) != null) {
return typeMapping.get(schema.getType());
}
}
return ModelUtils.getType(schema);
}
/**
* Recursively generates schemas for nested maps and arrays.
* @param schema the schema to be processed
* @param visitedSchemas a set of schemas that have already been visited
* @return the processed schema
*/
private Schema generateNestedSchema(Schema schema, Set<Schema> visitedSchemas) {
if (visitedSchemas.contains(schema)) {
LOGGER.warn("Skipping recursive schema");
return schema;
}
if(ModelUtils.isArraySchema(schema)) {
Schema itemsSchema = ModelUtils.getSchemaItems(schema);
itemsSchema = ModelUtils.getReferencedSchema(openAPI, itemsSchema);
if(ModelUtils.isModel(itemsSchema)) {
String newSchemaName = ModelUtils.getSimpleRef(ModelUtils.getSchemaItems(schema).get$ref()) + ARRAY_SUFFIX;
return addSchemas(schema, newSchemaName, visitedSchemas);
}else if (ModelUtils.isPrimitiveType(itemsSchema)){
String newSchemaName = getNameFromSchemaPrimitiveType(itemsSchema) + ARRAY_SUFFIX;
return addSchemas(schema, newSchemaName, visitedSchemas);
} else {
Schema childSchema = generateNestedSchema(itemsSchema, visitedSchemas);
String newSchemaName = childSchema.getName() + ARRAY_SUFFIX;
Schema arrayModel = createArraySchema(childSchema);
return addSchemas(arrayModel, newSchemaName, visitedSchemas);
}
} else if(ModelUtils.isMapSchema(schema)) {
Schema mapValueSchema = ModelUtils.getAdditionalProperties(schema);
mapValueSchema = ModelUtils.getReferencedSchema(openAPI, mapValueSchema);
if(ModelUtils.isModel(mapValueSchema) ) {
String newSchemaName = ModelUtils.getSimpleRef(ModelUtils.getAdditionalProperties(schema).get$ref()) + MAP_SUFFIX;
return addSchemas(schema, newSchemaName, visitedSchemas);
}else if (ModelUtils.isPrimitiveType(mapValueSchema)){
String newSchemaName = getNameFromSchemaPrimitiveType(mapValueSchema) + MAP_SUFFIX;
return addSchemas(schema, newSchemaName, visitedSchemas);
} else {
Schema innerSchema = generateNestedSchema(mapValueSchema, visitedSchemas);
String newSchemaName = innerSchema.getName() + MAP_SUFFIX;
Schema mapModel = createMapSchema(innerSchema);
return addSchemas(mapModel, newSchemaName, visitedSchemas);
}
}
return schema;
}
/**
* Processes nested schemas for complex type(map, array, oneOf)
*
* @param schema the schema to be processed
* @param visitedSchemas a set of schemas that have already been visited
*/
private void processNestedSchemas(Schema schema, Set<Schema> visitedSchemas) {
if (ModelUtils.isMapSchema(schema) && ModelUtils.getAdditionalProperties(schema) != null) {
Schema mapValueSchema = ModelUtils.getAdditionalProperties(schema);
mapValueSchema = ModelUtils.getReferencedSchema(openAPI, mapValueSchema);
if (ModelUtils.isArraySchema(mapValueSchema) || ModelUtils.isMapSchema(mapValueSchema)) {
Schema innerSchema = generateNestedSchema(mapValueSchema, visitedSchemas);
schema.setAdditionalProperties(innerSchema);
}
} else if (ModelUtils.isArraySchema(schema) && ModelUtils.getSchemaItems(schema) != null) {
Schema arrayItemSchema = ModelUtils.getSchemaItems(schema);
arrayItemSchema = ModelUtils.getReferencedSchema(openAPI, arrayItemSchema);
if (ModelUtils.isMapSchema(arrayItemSchema) || ModelUtils.isArraySchema(arrayItemSchema)) {
Schema innerSchema = generateNestedSchema(arrayItemSchema, visitedSchemas);
schema.setItems(innerSchema);
}
} else if (ModelUtils.isOneOf(schema) && schema.getOneOf() != null) {
List<Schema> oneOfs = schema.getOneOf();
List<Schema> newOneOfs = new ArrayList<>();
for (Schema oneOf : oneOfs) {
Schema oneOfSchema = ModelUtils.getReferencedSchema(openAPI, oneOf);
if (ModelUtils.isArraySchema(oneOfSchema)) {
Schema innerSchema = generateNestedSchema(oneOfSchema, visitedSchemas);
innerSchema.setTitle(oneOf.getTitle());
newOneOfs.add(innerSchema);
} else if (ModelUtils.isMapSchema(oneOfSchema)) {
Schema innerSchema = generateNestedSchema(oneOfSchema, visitedSchemas);
innerSchema.setTitle(oneOf.getTitle());
newOneOfs.add(innerSchema);
} else {
newOneOfs.add(oneOf);
}
}
schema.setOneOf(newOneOfs);
}
}
/**
* Traverses models and properties to wrap nested schemas.
*/
private void wrapModels() {
Map<String, Schema> models = openAPI.getComponents().getSchemas();
Set<Schema> visitedSchema = new HashSet<>();
List<String> modelNames = new ArrayList<String>(models.keySet());
for (String modelName: modelNames) {
Schema schema = models.get(modelName);
processNestedSchemas(schema, visitedSchema);
if (ModelUtils.isModel(schema) && schema.getProperties() != null) {
Map<String, Schema> properties = schema.getProperties();
for (Map.Entry<String, Schema> propertyEntry : properties.entrySet()) {
Schema propertySchema = propertyEntry.getValue();
processNestedSchemas(propertySchema, visitedSchema);
}
} else if (ModelUtils.isAllOf(schema)) {
wrapComposedChildren(schema.getAllOf(), visitedSchema);
} else if (ModelUtils.isOneOf(schema)) {
wrapComposedChildren(schema.getOneOf(), visitedSchema);
} else if (ModelUtils.isAnyOf(schema)) {
wrapComposedChildren(schema.getAnyOf(), visitedSchema);
}
}
}
/**
* Traverses a composed schema and its properties to wrap nested schemas.
*
* @param children the list of child schemas to be processed
* @param visitedSchema a set of schemas that have already been visited
*/
private void wrapComposedChildren(List<Schema> children, Set<Schema> visitedSchema) {
if (children == null || children.isEmpty()) {
return;
}
for(Schema child: children) {
child = ModelUtils.getReferencedSchema(openAPI, child);
Map<String, Schema> properties = child.getProperties();
if(properties == null || properties.isEmpty()) continue;
for(Map.Entry<String, Schema> propertyEntry : properties.entrySet()) {
Schema propertySchema = propertyEntry.getValue();
processNestedSchemas(propertySchema, visitedSchema);
}
}
}
@Override
public void preprocessOpenAPI(OpenAPI openAPI) {
super.preprocessOpenAPI(openAPI);
if (wrapComplexType) {
wrapModels();
}
}
/** /**
* Adds prefix to the enum allowable values * Adds prefix to the enum allowable values
* NOTE: Enum values use C++ scoping rules, meaning that enum values are siblings of their type, not children of it. Therefore, enum value must be unique * NOTE: Enum values use C++ scoping rules, meaning that enum values are siblings of their type, not children of it. Therefore, enum value must be unique

View File

@ -0,0 +1,130 @@
openapi: 3.1.0
servers:
- url: 'http://petstore.swagger.io/v2'
info:
description: >-
This is a sample server Petstore server. For this sample, you can use the api key
`special-key` to test the authorization filters.
version: 1.0.0
title: OpenAPI Petstore
license:
name: Apache-2.0
url: 'https://www.apache.org/licenses/LICENSE-2.0.html'
tags:
- name: pet
description: Everything about your Pets
- name: store
description: Access to Petstore orders
- name: user
description: Operations about user
paths:
/pets:
get:
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Cat"
responses:
"200":
description: Updated
externalDocs:
description: Find out more about Swagger
url: 'http://swagger.io'
components:
schemas:
Dog:
type: object
properties:
bark:
type: boolean
breed:
type: string
enum: [ Dingo, Husky, Retriever, Shepherd ]
Cat:
type: object
properties:
hunts:
type: boolean
age:
type: integer
Category:
title: Pet category
description: A category for a pet
type: object
properties:
id:
type: integer
format: int64
name:
type: string
pattern: '^[a-zA-Z0-9]+[a-zA-Z0-9\.\-_]*[a-zA-Z0-9]+$'
document:
type: object
additionalProperties:
type: array
items:
type: string
xml:
name: Category
Tag:
title: Pet Tag
description: A tag for a pet
type: object
properties:
id:
type: integer
format: int64
name:
oneOf:
- type: string
- type: array
items:
type: string
- type: object
title: map
additionalProperties:
type: string
xml:
name: Tag
Pet:
title: a Pet
description: A pet for sale in the pet store
type: object
required:
- name
- photoUrls
properties:
id:
type: integer
format: int64
category:
$ref: '#/components/schemas/Category'
name:
type: string
photoUrls:
type: array
xml:
name: photoUrl
wrapped: true
items:
type: string
tags:
type: array
xml:
name: tag
wrapped: true
items:
type: array
additionalProperties:
$ref: '#/components/schemas/Tag'
status:
type: string
description: pet status in the store
deprecated: true
enum:
- available
- pending
- sold
xml:
name: Pet

View File

@ -0,0 +1,23 @@
# OpenAPI Generator Ignore
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.
# As an example, the C# client generator defines ApiClient.cs.
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
#ApiClient.cs
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
#foo/*/qux
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
#foo/**/qux
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
# You can also negate patterns with an exclamation (!).
# For example, you can ignore all files in a docs folder with the file extension .md:
#docs/*.md
# Then explicitly reverse the ignore rule for a single file:
#!docs/README.md

View File

@ -0,0 +1,10 @@
README.md
models/cat.proto
models/category.proto
models/dog.proto
models/pet.proto
models/string_array.proto
models/string_map.proto
models/tag.proto
models/tag_name.proto
services/default_service.proto

View File

@ -0,0 +1 @@
7.13.0-SNAPSHOT

View File

@ -0,0 +1,32 @@
# gPRC for petstore
This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters.
## Overview
These files were generated by the [OpenAPI Generator](https://openapi-generator.tech) project.
- API version: 1.0.0
- Package version:
- Generator version: 7.13.0-SNAPSHOT
- Build package: org.openapitools.codegen.languages.ProtobufSchemaCodegen
## Usage
Below are some usage examples for Go and Ruby. For other languages, please refer to https://grpc.io/docs/quickstart/.
### Go
```
# assuming `protoc-gen-go` has been installed with `go get -u github.com/golang/protobuf/protoc-gen-go`
mkdir /var/tmp/go/petstore
protoc --go_out=/var/tmp/go/petstore services/*
protoc --go_out=/var/tmp/go/petstore models/*
```
### Ruby
```
# assuming `grpc_tools_ruby_protoc` has been installed via `gem install grpc-tools`
RUBY_OUTPUT_DIR="/var/tmp/ruby/petstore"
mkdir $RUBY_OUTPUT_DIR
grpc_tools_ruby_protoc --ruby_out=$RUBY_OUTPUT_DIR --grpc_out=$RUBY_OUTPUT_DIR/lib services/*
grpc_tools_ruby_protoc --ruby_out=$RUBY_OUTPUT_DIR --grpc_out=$RUBY_OUTPUT_DIR/lib models/*
```

View File

@ -13,12 +13,10 @@ syntax = "proto3";
package petstore; package petstore;
message InlineObject { message Cat {
// Updated name of the pet bool hunts = 1;
string name = 3373707;
// Updated status of the pet int32 age = 2;
string status = 355610639;
} }

View File

@ -12,13 +12,14 @@ syntax = "proto3";
package petstore; package petstore;
import public "models/string_array.proto";
message InlineObject1 { message Category {
// Additional data to pass to server int64 id = 1;
string additionalMetadata = 400408697;
// file to upload string name = 2;
string file = 3143036;
map<string, StringArray> document = 3;
} }

View File

@ -0,0 +1,30 @@
/*
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
Generated by OpenAPI Generator: https://openapi-generator.tech
*/
syntax = "proto3";
package petstore;
message Dog {
bool bark = 1;
enum Breed {
BREED_UNSPECIFIED = 0;
BREED_DINGO = 1;
BREED_HUSKY = 2;
BREED_RETRIEVER = 3;
BREED_SHEPHERD = 4;
}
Breed breed = 2;
}

View File

@ -0,0 +1,23 @@
/*
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
Generated by OpenAPI Generator: https://openapi-generator.tech
*/
syntax = "proto3";
package petstore;
import public "models/category.proto";
message Pet {
int64 id = 1;
Category category = 2;
}

View File

@ -0,0 +1,20 @@
/*
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
Generated by OpenAPI Generator: https://openapi-generator.tech
*/
syntax = "proto3";
package petstore;
message StringArray {
repeated string string_array = 1;
}

View File

@ -0,0 +1,20 @@
/*
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
Generated by OpenAPI Generator: https://openapi-generator.tech
*/
syntax = "proto3";
package petstore;
message StringMap {
map<string, string> string_map = 1;
}

View File

@ -0,0 +1,23 @@
/*
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
Generated by OpenAPI Generator: https://openapi-generator.tech
*/
syntax = "proto3";
package petstore;
import public "models/tag_name.proto";
message Tag {
int64 id = 1;
TagName name = 2;
}

View File

@ -0,0 +1,25 @@
/*
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
Generated by OpenAPI Generator: https://openapi-generator.tech
*/
syntax = "proto3";
package petstore;
import public "models/string_array.proto";
import public "models/string_map.proto";
message TagName {
oneof tag_name {
string string = 1;
StringArray string_array = 2;
StringMap map = 3;
}
}

View File

@ -0,0 +1,27 @@
/*
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
Generated by OpenAPI Generator: https://openapi-generator.tech
*/
syntax = "proto3";
package petstore.services.defaultservice;
import "google/protobuf/empty.proto";
import public "models/cat.proto";
service DefaultService {
rpc PetsGet (PetsGetRequest) returns (google.protobuf.Empty);
}
message PetsGetRequest {
Cat cat = 1;
}