diff --git a/internal/descriptor/registry.go b/internal/descriptor/registry.go index e6f78482ef1..3a35dea6229 100644 --- a/internal/descriptor/registry.go +++ b/internal/descriptor/registry.go @@ -87,6 +87,9 @@ type Registry struct { // has no HttpRule annotation. warnOnUnboundMethods bool + // proto3OptionalNullable specifies whether Proto3 Optional fields should be marked as x-nullable. + proto3OptionalNullable bool + // fileOptions is a mapping of file name to additional OpenAPI file options fileOptions map[string]*options.Swagger @@ -568,6 +571,16 @@ func (r *Registry) GetOmitPackageDoc() bool { return r.omitPackageDoc } +// SetProto3OptionalNullable set proto3OtionalNullable +func (r *Registry) SetProto3OptionalNullable(proto3OtionalNullable bool) { + r.proto3OptionalNullable = proto3OtionalNullable +} + +// GetProto3OptionalNullable returns proto3OtionalNullable +func (r *Registry) GetProto3OptionalNullable() bool { + return r.proto3OptionalNullable +} + // RegisterOpenAPIOptions registers OpenAPI options func (r *Registry) RegisterOpenAPIOptions(opts *openapiconfig.OpenAPIOptions) error { if opts == nil { diff --git a/protoc-gen-openapiv2/defs.bzl b/protoc-gen-openapiv2/defs.bzl index ea3226762a5..43596664d55 100644 --- a/protoc-gen-openapiv2/defs.bzl +++ b/protoc-gen-openapiv2/defs.bzl @@ -62,6 +62,7 @@ def _run_proto_gen_openapi( disable_default_errors, enums_as_ints, simple_operation_ids, + proto3_optional_nullable, openapi_configuration, generate_unbound_methods): args = actions.args() @@ -107,6 +108,9 @@ def _run_proto_gen_openapi( if enums_as_ints: args.add("--openapiv2_opt", "enums_as_ints=true") + if proto3_optional_nullable: + args.add("--openapiv2_opt", "proto3_optional_nullable=true") + args.add("--openapiv2_opt", "repeated_path_param_separator=%s" % repeated_path_param_separator) proto_file_infos = _direct_source_infos(proto_info) @@ -201,6 +205,7 @@ def _proto_gen_openapi_impl(ctx): disable_default_errors = ctx.attr.disable_default_errors, enums_as_ints = ctx.attr.enums_as_ints, simple_operation_ids = ctx.attr.simple_operation_ids, + proto3_optional_nullable = ctx.attr.proto3_optional_nullable, openapi_configuration = ctx.file.openapi_configuration, generate_unbound_methods = ctx.attr.generate_unbound_methods, ), @@ -278,6 +283,11 @@ protoc_gen_openapiv2 = rule( doc = "whether to remove the service prefix in the operationID" + " generation. Can introduce duplicate operationIDs, use with caution.", ), + "proto3_optional_nullable": attr.bool( + default = False, + mandatory = False, + doc = "whether Proto3 Optional fields should be marked as x-nullable", + ), "openapi_configuration": attr.label( allow_single_file = True, mandatory = False, diff --git a/protoc-gen-openapiv2/internal/genopenapi/template.go b/protoc-gen-openapiv2/internal/genopenapi/template.go index 0ac0d64495c..ff0ef171d18 100644 --- a/protoc-gen-openapiv2/internal/genopenapi/template.go +++ b/protoc-gen-openapiv2/internal/genopenapi/template.go @@ -410,6 +410,7 @@ func renderMessageAsDefinition(msg *descriptor.Message, reg *descriptor.Registry schema.MaxProperties = protoSchema.MaxProperties schema.MinProperties = protoSchema.MinProperties schema.Required = protoSchema.Required + schema.XNullable = protoSchema.XNullable if protoSchema.schemaCore.Type != "" || protoSchema.schemaCore.Ref != "" { schema.schemaCore = protoSchema.schemaCore } @@ -581,6 +582,10 @@ func schemaOfField(f *descriptor.Field, reg *descriptor.Registry, refs refMap) o updateSwaggerObjectFromFieldBehavior(&ret, j, f) } + if reg.GetProto3OptionalNullable() && f.GetProto3Optional() { + ret.XNullable = true + } + return ret } diff --git a/protoc-gen-openapiv2/internal/genopenapi/types.go b/protoc-gen-openapiv2/internal/genopenapi/types.go index 505cfc686e8..861c4c73338 100644 --- a/protoc-gen-openapiv2/internal/genopenapi/types.go +++ b/protoc-gen-openapiv2/internal/genopenapi/types.go @@ -150,12 +150,18 @@ type openapiParameterObject struct { } // core part of schema, which is common to itemsObject and schemaObject. -// http://swagger.io/specification/#itemsObject +// http://swagger.io/specification/v2/#itemsObject +// The OAS3 spec (https://swagger.io/specification/#schemaObject) defines the +// `nullable` field as part of a Schema Object. This behavior has been +// "back-ported" to OAS2 as the Specification Extension `x-nullable`, and is +// supported by generation tools such as swagger-codegen and go-swagger. +// For protoc-gen-openapiv3, we'd want to add `nullable` instead. type schemaCore struct { - Type string `json:"type,omitempty"` - Format string `json:"format,omitempty"` - Ref string `json:"$ref,omitempty"` - Example json.RawMessage `json:"example,omitempty"` + Type string `json:"type,omitempty"` + Format string `json:"format,omitempty"` + Ref string `json:"$ref,omitempty"` + XNullable bool `json:"x-nullable,omitempty"` + Example json.RawMessage `json:"example,omitempty"` Items *openapiItemsObject `json:"items,omitempty"` diff --git a/protoc-gen-openapiv2/main.go b/protoc-gen-openapiv2/main.go index b05af0e879d..ff104d9f842 100644 --- a/protoc-gen-openapiv2/main.go +++ b/protoc-gen-openapiv2/main.go @@ -31,6 +31,7 @@ var ( disableDefaultErrors = flag.Bool("disable_default_errors", false, "if set, disables generation of default errors. This is useful if you have defined custom error handling") enumsAsInts = flag.Bool("enums_as_ints", false, "whether to render enum values as integers, as opposed to string values") simpleOperationIDs = flag.Bool("simple_operation_ids", false, "whether to remove the service prefix in the operationID generation. Can introduce duplicate operationIDs, use with caution.") + proto3OptionalNullable = flag.Bool("proto3_optional_nullable", false, "whether Proto3 Optional fields should be marked as x-nullable") openAPIConfiguration = flag.String("openapi_configuration", "", "path to file which describes the OpenAPI Configuration in YAML format") generateUnboundMethods = flag.Bool("generate_unbound_methods", false, "generate swagger metadata even for RPC methods that have no HttpRule annotation") recursiveDepth = flag.Int("recursive-depth", 1000, "maximum recursion count allowed for a field type") @@ -89,6 +90,7 @@ func main() { reg.SetEnumsAsInts(*enumsAsInts) reg.SetDisableDefaultErrors(*disableDefaultErrors) reg.SetSimpleOperationIDs(*simpleOperationIDs) + reg.SetProto3OptionalNullable(*proto3OptionalNullable) reg.SetGenerateUnboundMethods(*generateUnboundMethods) reg.SetRecursiveDepth(*recursiveDepth) if err := reg.SetRepeatedPathParamSeparator(*repeatedPathParamSeparator); err != nil {