Skip to content

Commit fb73506

Browse files
authored
Merge pull request #8 from fern-api/cd/json-schema-one-of-support
support oneOf
2 parents 5928965 + f8f3042 commit fb73506

10 files changed

Lines changed: 1026 additions & 58 deletions

File tree

cmd/protoc-gen-jsonschema/generator/json-schema.go

Lines changed: 170 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ var (
3636
typeBoolean = "boolean"
3737
typeObject = "object"
3838
typeArray = "array"
39+
typeNull = "null"
3940

4041
formatDate = "date"
4142
formatDateTime = "date-time"
@@ -127,6 +128,23 @@ func (g *JSONSchemaGenerator) formatFieldName(field *protogen.Field) string {
127128
return field.Desc.JSONName()
128129
}
129130

131+
func (g *JSONSchemaGenerator) formatOneofFieldName(oneof *protogen.Oneof) string {
132+
if *g.conf.Naming == "proto" {
133+
return string(oneof.Desc.Name())
134+
}
135+
136+
name := oneof.GoName
137+
if len(name) > 1 {
138+
return strings.ToLower(name[0:1]) + name[1:]
139+
}
140+
141+
if len(name) == 1 {
142+
return strings.ToLower(name)
143+
}
144+
145+
return name
146+
}
147+
130148
// messageDefinitionName builds the full schema definition name of a message.
131149
func messageDefinitionName(desc protoreflect.MessageDescriptor) string {
132150
name := string(desc.Name())
@@ -262,31 +280,158 @@ func (g *JSONSchemaGenerator) schemaOrReferenceForField(field protoreflect.Field
262280
return kindSchema
263281
}
264282

283+
func (g *JSONSchemaGenerator) namedSchemaForField(field *protogen.Field, schema *jsonschema.NamedSchema, isValueProp bool) *jsonschema.NamedSchema {
284+
// The field is either described by a reference or a schema.
285+
fieldSchema := g.schemaOrReferenceForField(field.Desc, schema.Value.Definitions)
286+
if fieldSchema == nil {
287+
return nil
288+
}
289+
290+
// Handle readonly and writeonly properties, if the schema version can handle it.
291+
if getSchemaVersion(schema.Value) >= "07" {
292+
t := true
293+
// Check the field annotations to see if this is a readonly field.
294+
extension := proto.GetExtension(field.Desc.Options(), annotations.E_FieldBehavior)
295+
if extension != nil {
296+
switch v := extension.(type) {
297+
case []annotations.FieldBehavior:
298+
for _, vv := range v {
299+
if vv == annotations.FieldBehavior_OUTPUT_ONLY {
300+
fieldSchema.ReadOnly = &t
301+
} else if vv == annotations.FieldBehavior_INPUT_ONLY {
302+
fieldSchema.WriteOnly = &t
303+
}
304+
}
305+
default:
306+
log.Printf("unsupported extension type %T", extension)
307+
}
308+
}
309+
}
310+
311+
fieldName := "value"
312+
if !isValueProp {
313+
fieldName = g.formatFieldName(field)
314+
}
315+
316+
// Do not add title for ref values
317+
if fieldSchema.Ref == nil {
318+
fieldSchema.Title = &fieldName
319+
}
320+
321+
// Get the field description from the comments.
322+
description := g.filterCommentString(field.Comments.Leading, true)
323+
if description != "" {
324+
// Note: Description will be ignored if $ref is set, but is still useful
325+
fieldSchema.Description = &description
326+
}
327+
328+
return &jsonschema.NamedSchema{
329+
Name: fieldName,
330+
Value: fieldSchema,
331+
}
332+
}
333+
334+
func (g *JSONSchemaGenerator) setupSchemaForMessage(schemaName string, comments protogen.Comments) *jsonschema.NamedSchema {
335+
typ := "object"
336+
id := fmt.Sprintf("%s%s.json", *g.conf.BaseURL, schemaName)
337+
338+
schema := &jsonschema.NamedSchema{
339+
Name: schemaName,
340+
Value: &jsonschema.Schema{
341+
Schema: g.conf.Version,
342+
ID: &id,
343+
Type: &jsonschema.StringOrStringArray{String: &typ},
344+
Title: &schemaName,
345+
Properties: &[]*jsonschema.NamedSchema{},
346+
},
347+
}
348+
349+
description := g.filterCommentString(comments, true)
350+
if description != "" {
351+
schema.Value.Description = &description
352+
}
353+
354+
return schema
355+
}
356+
357+
func (g *JSONSchemaGenerator) buildKindProperty(propertyValue string) *jsonschema.NamedSchema {
358+
kind := "kind"
359+
kindProperty := &jsonschema.NamedSchema{
360+
Name: kind,
361+
Value: &jsonschema.Schema{
362+
Title: &kind,
363+
Type: &jsonschema.StringOrStringArray{String: &typeString},
364+
Enumeration: &[]jsonschema.SchemaEnumValue{},
365+
},
366+
}
367+
*kindProperty.Value.Enumeration = append(
368+
*kindProperty.Value.Enumeration,
369+
jsonschema.SchemaEnumValue{String: &propertyValue},
370+
)
371+
return kindProperty
372+
}
373+
374+
func (g *JSONSchemaGenerator) addOneofFieldsToSchema(oneofs []*protogen.Oneof, schema *jsonschema.NamedSchema) {
375+
if oneofs == nil {
376+
return
377+
}
378+
379+
for _, oneOfProto := range oneofs {
380+
oneOfSchema := jsonschema.Schema{
381+
OneOf: &[]*jsonschema.Schema{},
382+
}
383+
384+
*oneOfSchema.OneOf = append(*oneOfSchema.OneOf, &jsonschema.Schema{Type: &jsonschema.StringOrStringArray{String: &typeNull}})
385+
386+
for _, fieldProto := range oneOfProto.Fields {
387+
ref := schema.Name + "_" + fieldProto.GoName
388+
oneofFieldSchema := &jsonschema.NamedSchema{
389+
Name: ref,
390+
Value: &jsonschema.Schema{
391+
Type: &jsonschema.StringOrStringArray{String: &typeObject},
392+
Title: &ref,
393+
Properties: &[]*jsonschema.NamedSchema{},
394+
},
395+
}
396+
kindProperty := g.buildKindProperty(string(fieldProto.Desc.Name()))
397+
actualProperty := g.namedSchemaForField(fieldProto, schema, true)
398+
if actualProperty == nil {
399+
continue
400+
}
401+
402+
*oneofFieldSchema.Value.Properties = append(
403+
*oneofFieldSchema.Value.Properties,
404+
kindProperty,
405+
actualProperty,
406+
)
407+
408+
if schema.Value.Definitions == nil {
409+
schema.Value.Definitions = &[]*jsonschema.NamedSchema{}
410+
}
411+
*schema.Value.Definitions = append(*schema.Value.Definitions, oneofFieldSchema)
412+
413+
definitionsRef := "#/definitions/" + ref
414+
*oneOfSchema.OneOf = append(*oneOfSchema.OneOf, &jsonschema.Schema{Ref: &definitionsRef})
415+
}
416+
417+
*schema.Value.Properties = append(
418+
*schema.Value.Properties,
419+
&jsonschema.NamedSchema{
420+
Name: g.formatOneofFieldName(oneOfProto),
421+
Value: &oneOfSchema,
422+
},
423+
)
424+
}
425+
}
426+
265427
// buildSchemasFromMessages creates a schema for each message.
266428
func (g *JSONSchemaGenerator) buildSchemasFromMessages(messages []*protogen.Message) []*jsonschema.NamedSchema {
267429
schemas := []*jsonschema.NamedSchema{}
268430

269431
// For each message, generate a schema.
270432
for _, message := range messages {
271433
schemaName := string(message.Desc.Name())
272-
typ := "object"
273-
id := fmt.Sprintf("%s%s.json", *g.conf.BaseURL, schemaName)
274-
275-
schema := &jsonschema.NamedSchema{
276-
Name: schemaName,
277-
Value: &jsonschema.Schema{
278-
Schema: g.conf.Version,
279-
ID: &id,
280-
Type: &jsonschema.StringOrStringArray{String: &typ},
281-
Title: &schemaName,
282-
Properties: &[]*jsonschema.NamedSchema{},
283-
},
284-
}
285-
286-
description := g.filterCommentString(message.Comments.Leading, true)
287-
if description != "" {
288-
schema.Value.Description = &description
289-
}
434+
schema := g.setupSchemaForMessage(schemaName, message.Comments.Leading)
290435

291436
// Any embedded messages will be created as definitions
292437
if message.Messages != nil {
@@ -316,54 +461,22 @@ func (g *JSONSchemaGenerator) buildSchemasFromMessages(messages []*protogen.Mess
316461
if message.Desc.IsMapEntry() {
317462
continue
318463
}
464+
465+
g.addOneofFieldsToSchema(message.Oneofs, schema)
319466

320467
for _, field := range message.Fields {
321-
// The field is either described by a reference or a schema.
322-
fieldSchema := g.schemaOrReferenceForField(field.Desc, schema.Value.Definitions)
323-
if fieldSchema == nil {
468+
if field.Oneof != nil {
324469
continue
325470
}
326471

327-
// Handle readonly and writeonly properties, if the schema version can handle it.
328-
if getSchemaVersion(schema.Value) >= "07" {
329-
t := true
330-
// Check the field annotations to see if this is a readonly field.
331-
extension := proto.GetExtension(field.Desc.Options(), annotations.E_FieldBehavior)
332-
if extension != nil {
333-
switch v := extension.(type) {
334-
case []annotations.FieldBehavior:
335-
for _, vv := range v {
336-
if vv == annotations.FieldBehavior_OUTPUT_ONLY {
337-
fieldSchema.ReadOnly = &t
338-
} else if vv == annotations.FieldBehavior_INPUT_ONLY {
339-
fieldSchema.WriteOnly = &t
340-
}
341-
}
342-
default:
343-
log.Printf("unsupported extension type %T", extension)
344-
}
345-
}
346-
}
347-
348-
fieldName := g.formatFieldName(field)
349-
// Do not add title for ref values
350-
if fieldSchema.Ref == nil {
351-
fieldSchema.Title = &fieldName
352-
}
353-
354-
// Get the field description from the comments.
355-
description := g.filterCommentString(field.Comments.Leading, true)
356-
if description != "" {
357-
// Note: Description will be ignored if $ref is set, but is still useful
358-
fieldSchema.Description = &description
472+
namedSchema := g.namedSchemaForField(field, schema, false)
473+
if namedSchema == nil {
474+
continue
359475
}
360476

361477
*schema.Value.Properties = append(
362478
*schema.Value.Properties,
363-
&jsonschema.NamedSchema{
364-
Name: fieldName,
365-
Value: fieldSchema,
366-
},
479+
namedSchema,
367480
)
368481
}
369482

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
syntax = "proto3";
2+
3+
package examples.oneof;
4+
5+
import "google/api/annotations.proto";
6+
7+
option go_package = "github.com/google/gnostic/examples/oneof";
8+
9+
// Message demonstrating oneof field usage
10+
message OneOfMessage {
11+
// Identifier for the message
12+
string id = 1;
13+
14+
// Example of oneof field - only one of these can be set
15+
oneof payment_method {
16+
string credit_card = 2;
17+
string bank_transfer = 3;
18+
string digital_wallet = 4;
19+
}
20+
21+
// Another oneof example with different types
22+
oneof contact_info {
23+
string email = 5;
24+
string phone = 6;
25+
string address = 7;
26+
}
27+
}
28+
29+
// Service definition
30+
service OneOfService {
31+
rpc CreatePayment(OneOfMessage) returns (OneOfMessage) {
32+
option (google.api.http) = {
33+
post: "/v1/payments"
34+
body: "*"
35+
};
36+
}
37+
}

0 commit comments

Comments
 (0)