Skip to content

Commit 5101e22

Browse files
authored
Also generate json schema using the JSON name as the primary name (#19)
* Add .jsonschema.json files that use the jsonNames as the primary field name * lint
1 parent fd4028e commit 5101e22

24 files changed

+6215
-50
lines changed

internal/cmd/jsonschema-generate-testdata/main.go

+31-22
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
_ "github.com/bufbuild/protoschema-plugins/internal/gen/proto/bufext/cel/expr/conformance/proto3"
2525
"github.com/bufbuild/protoschema-plugins/internal/protoschema/golden"
2626
"github.com/bufbuild/protoschema-plugins/internal/protoschema/jsonschema"
27+
"google.golang.org/protobuf/reflect/protoreflect"
2728
)
2829

2930
func main() {
@@ -40,37 +41,45 @@ func run() error {
4041
return fmt.Errorf("usage: %s [output dir]", os.Args[0])
4142
}
4243
outputDir := os.Args[1]
44+
// Make sure the directory exists
45+
if err := os.MkdirAll(outputDir, 0755); err != nil {
46+
return err
47+
}
4348

4449
testDescs, err := golden.GetTestDescriptors("./internal/testdata")
4550
if err != nil {
4651
return err
4752
}
4853
for _, testDesc := range testDescs {
49-
// Generate the JSON schema
50-
result := jsonschema.Generate(testDesc)
51-
52-
// Make sure the directory exists
53-
if err := os.MkdirAll(outputDir, 0755); err != nil {
54+
// Generate the JSON schema with proto names.
55+
if err := writeJSONSchema(outputDir, jsonschema.Generate(testDesc)); err != nil {
5456
return err
5557
}
58+
// Generate the JSON schema with JSON names.
59+
if err := writeJSONSchema(outputDir, jsonschema.Generate(testDesc, jsonschema.WithJSONNames())); err != nil {
60+
return err
61+
}
62+
}
63+
return nil
64+
}
5665

57-
for _, jsonSchema := range result {
58-
// Serialize the JSON
59-
data, err := json.MarshalIndent(jsonSchema, "", " ")
60-
if err != nil {
61-
return err
62-
}
63-
identifier, ok := jsonSchema["$id"].(string)
64-
if !ok {
65-
return errors.New("expected $id to be a string")
66-
}
67-
if identifier == "" {
68-
return errors.New("expected $id to be non-empty")
69-
}
70-
filePath := filepath.Join(outputDir, identifier)
71-
if err := golden.GenerateGolden(filePath, string(data)+"\n"); err != nil {
72-
return err
73-
}
66+
func writeJSONSchema(outputDir string, schema map[protoreflect.FullName]map[string]interface{}) error {
67+
for _, jsonSchema := range schema {
68+
// Serialize the JSON
69+
data, err := json.MarshalIndent(jsonSchema, "", " ")
70+
if err != nil {
71+
return err
72+
}
73+
identifier, ok := jsonSchema["$id"].(string)
74+
if !ok {
75+
return errors.New("expected $id to be a string")
76+
}
77+
if identifier == "" {
78+
return errors.New("expected $id to be non-empty")
79+
}
80+
filePath := filepath.Join(outputDir, identifier)
81+
if err := golden.GenerateGolden(filePath, string(data)+"\n"); err != nil {
82+
return err
7483
}
7584
}
7685
return nil

internal/protoschema/jsonschema/jsonschema.go

+30-7
Original file line numberDiff line numberDiff line change
@@ -41,22 +41,38 @@ const (
4141
FieldIgnore
4242
)
4343

44-
// Generate generates a JSON schema for the given message descriptor.
45-
func Generate(input protoreflect.MessageDescriptor) map[protoreflect.FullName]map[string]interface{} {
44+
type GeneratorOption func(*jsonSchemaGenerator)
45+
46+
// WithJSONNames sets the generator to use JSON field names as the primary name.
47+
func WithJSONNames() GeneratorOption {
48+
return func(p *jsonSchemaGenerator) {
49+
p.useJSONNames = true
50+
}
51+
}
52+
53+
// Generate generates a JSON schema for the given message descriptor, with protobuf field names.
54+
func Generate(input protoreflect.MessageDescriptor, opts ...GeneratorOption) map[protoreflect.FullName]map[string]interface{} {
4655
generator := &jsonSchemaGenerator{
4756
result: make(map[protoreflect.FullName]map[string]interface{}),
4857
}
4958
generator.custom = generator.makeWktGenerators()
59+
for _, opt := range opts {
60+
opt(generator)
61+
}
5062
generator.generate(input)
5163
return generator.result
5264
}
5365

5466
type jsonSchemaGenerator struct {
55-
result map[protoreflect.FullName]map[string]interface{}
56-
custom map[protoreflect.FullName]func(map[string]interface{}, protoreflect.MessageDescriptor)
67+
result map[protoreflect.FullName]map[string]interface{}
68+
custom map[protoreflect.FullName]func(map[string]interface{}, protoreflect.MessageDescriptor)
69+
useJSONNames bool
5770
}
5871

5972
func (p *jsonSchemaGenerator) getID(desc protoreflect.Descriptor) string {
73+
if p.useJSONNames {
74+
return string(desc.FullName()) + ".jsonschema.json"
75+
}
6076
return string(desc.FullName()) + ".schema.json"
6177
}
6278

@@ -94,13 +110,20 @@ func (p *jsonSchemaGenerator) generateDefault(result map[string]interface{}, des
94110
// TODO: Add an option to include custom alias.
95111
aliases := make([]string, 0, 1)
96112

97-
if visibility == FieldHide {
113+
switch {
114+
case visibility == FieldHide:
98115
aliases = append(aliases, string(field.Name()))
99116
if field.JSONName() != string(field.Name()) {
100117
aliases = append(aliases, field.JSONName())
101118
}
102-
} else {
103-
// TODO: Optionally make the json name the 'primary' name.
119+
case p.useJSONNames:
120+
// Use the JSON name as the primary name.
121+
properties[field.JSONName()] = fieldSchema
122+
if field.JSONName() != string(field.Name()) {
123+
aliases = append(aliases, string(field.Name()))
124+
}
125+
default:
126+
// Use the proto name as the primary name.
104127
properties[string(field.Name())] = fieldSchema
105128
if field.JSONName() != string(field.Name()) {
106129
aliases = append(aliases, field.JSONName())

internal/protoschema/plugin/pluginjsonschema/pluginjsonschema.go

+38-21
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121

2222
"github.com/bufbuild/protoplugin"
2323
"github.com/bufbuild/protoschema-plugins/internal/protoschema/jsonschema"
24+
"google.golang.org/protobuf/reflect/protoreflect"
2425
)
2526

2627
// Handle implements protoplugin.Handler and is the main entry point for the plugin.
@@ -38,31 +39,47 @@ func Handle(
3839
for _, fileDescriptor := range fileDescriptors {
3940
for i := range fileDescriptor.Messages().Len() {
4041
messageDescriptor := fileDescriptor.Messages().Get(i)
41-
42-
for _, entry := range jsonschema.Generate(messageDescriptor) {
43-
data, err := json.MarshalIndent(entry, "", " ")
44-
if err != nil {
45-
return err
46-
}
47-
identifier, ok := entry["$id"].(string)
48-
if !ok {
49-
return fmt.Errorf("expected unique id for message %q to be a string, got type %T", messageDescriptor.FullName(), entry["$id"])
50-
}
51-
if identifier == "" {
52-
return fmt.Errorf("expected unique id for message %q to be a non-empty string", messageDescriptor.FullName())
53-
}
54-
if seenIdentifiers[identifier] {
55-
continue
56-
}
57-
responseWriter.AddFile(
58-
identifier,
59-
string(data)+"\n",
60-
)
61-
seenIdentifiers[identifier] = true
42+
protoNameSchema := jsonschema.Generate(messageDescriptor)
43+
if err := writeFiles(responseWriter, messageDescriptor, protoNameSchema, seenIdentifiers); err != nil {
44+
return err
45+
}
46+
jsonNameSchema := jsonschema.Generate(messageDescriptor, jsonschema.WithJSONNames())
47+
if err := writeFiles(responseWriter, messageDescriptor, jsonNameSchema, seenIdentifiers); err != nil {
48+
return err
6249
}
6350
}
6451
}
6552

6653
responseWriter.SetFeatureProto3Optional()
6754
return nil
6855
}
56+
57+
func writeFiles(
58+
responseWriter protoplugin.ResponseWriter,
59+
messageDescriptor protoreflect.MessageDescriptor,
60+
schema map[protoreflect.FullName]map[string]interface{},
61+
seenIdentifiers map[string]bool,
62+
) error {
63+
for _, entry := range schema {
64+
data, err := json.MarshalIndent(entry, "", " ")
65+
if err != nil {
66+
return err
67+
}
68+
identifier, ok := entry["$id"].(string)
69+
if !ok {
70+
return fmt.Errorf("expected unique id for message %q to be a string, got type %T", messageDescriptor.FullName(), entry["$id"])
71+
}
72+
if identifier == "" {
73+
return fmt.Errorf("expected unique id for message %q to be a non-empty string", messageDescriptor.FullName())
74+
}
75+
if seenIdentifiers[identifier] {
76+
continue
77+
}
78+
responseWriter.AddFile(
79+
identifier,
80+
string(data)+"\n",
81+
)
82+
seenIdentifiers[identifier] = true
83+
}
84+
return nil
85+
}

internal/testdata/jsonschema/buf.protoschema.test.v1.CustomOptions.jsonschema.json

+33
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/testdata/jsonschema/buf.protoschema.test.v1.IgnoreField.jsonschema.json

+25
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/testdata/jsonschema/buf.protoschema.test.v1.NestedReference.jsonschema.json

+17
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/testdata/jsonschema/bufext.cel.expr.conformance.proto3.NestedTestAllTypes.jsonschema.json

+16
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

internal/testdata/jsonschema/bufext.cel.expr.conformance.proto3.TestAllTypes.NestedMessage.jsonschema.json

+15
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)