Skip to content

Commit 7519cf6

Browse files
fenollpdiamondburned
authored andcommitted
Add openapi3.Schema.OrderedPropertyKeys
This commit adds the `OrderedPropertyKeys` method to the `openapi3.Schema`: OrderedPropertyKeys returns the keys of the properties in the order they were defined. This is useful for generating code that needs to iterate over the properties in a consistent order. If the keys could not be extracted for some reason, then this method automatically sorts the keys to be deterministic. This is done via a temporary fork of the YAML-to-JSON transformation library. It will not be ready until these PRs are merged in this order: - silasdavis/yaml#1 - ghodss/yaml#62
1 parent e9b36da commit 7519cf6

File tree

7 files changed

+141
-3
lines changed

7 files changed

+141
-3
lines changed

go.mod

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@ module github.com/getkin/kin-openapi
22

33
go 1.14
44

5+
replace github.com/ghodss/yaml/v2 => github.com/diamondburned/yaml/v2 v2.0.0-20240812065612-baf990d70122
6+
57
require (
68
github.com/ghodss/yaml v1.0.0
9+
github.com/ghodss/yaml/v2 v2.0.0-00010101000000-000000000000
710
github.com/go-openapi/jsonpointer v0.19.5
811
github.com/gorilla/mux v1.8.0
912
github.com/stretchr/testify v1.5.1
1013
gopkg.in/yaml.v2 v2.3.0
14+
gopkg.in/yaml.v3 v3.0.1 // indirect
1115
)

go.sum

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
22
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
33
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
4+
github.com/diamondburned/yaml/v2 v2.0.0-20240812065612-baf990d70122 h1:hOA7Z6xhY5sn50zMsuY9JhA0A1QMiO0z/Ltx7ZcqUCM=
5+
github.com/diamondburned/yaml/v2 v2.0.0-20240812065612-baf990d70122/go.mod h1:KkR1H6NtyEqVsGChMAaRwn4BkIX0dG683i7NgqX947Y=
46
github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk=
57
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
68
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
@@ -29,3 +31,6 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
2931
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
3032
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
3133
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
34+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
35+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
36+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

jsoninfo/orderedmap.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package jsoninfo
2+
3+
import (
4+
"bytes"
5+
"encoding/json"
6+
"fmt"
7+
"log"
8+
)
9+
10+
// ExtractObjectKeys extracts the keys of an object in a JSON string. The keys
11+
// are returned in the order they appear in the JSON string.
12+
func ExtractObjectKeys(b []byte) ([]string, error) {
13+
if !bytes.HasPrefix(b, []byte{'{'}) {
14+
return nil, fmt.Errorf("expected '{' at start of JSON object")
15+
}
16+
17+
dec := json.NewDecoder(bytes.NewReader(b))
18+
var keys []string
19+
20+
for dec.More() {
21+
// Read prop name
22+
t, err := dec.Token()
23+
if err != nil {
24+
log.Printf("Err: %v", err)
25+
break
26+
}
27+
28+
name, ok := t.(string)
29+
if !ok {
30+
continue // May be a delimeter
31+
}
32+
33+
keys = append(keys, name)
34+
35+
var whatever nullMessage
36+
dec.Decode(&whatever)
37+
}
38+
39+
return keys, nil
40+
}
41+
42+
// nullMessage implements json.Unmarshaler and does nothing with the given
43+
// value.
44+
type nullMessage struct{}
45+
46+
func (*nullMessage) UnmarshalJSON(data []byte) error { return nil }

jsoninfo/orderedmap_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package jsoninfo
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
)
7+
8+
func TestExtractObjectKeys(t *testing.T) {
9+
const j = `{
10+
"foo": {"bar": 1},
11+
"baz": "qux",
12+
"quux": "quuz"
13+
}`
14+
15+
keys, err := ExtractObjectKeys([]byte(j))
16+
if err != nil {
17+
t.Fatal(err)
18+
}
19+
20+
if !reflect.DeepEqual(keys, []string{"foo", "baz", "quux"}) {
21+
t.Fatalf("expected %v, got %v", []string{"foo", "baz", "quux"}, keys)
22+
}
23+
}

openapi3/loader.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
"strconv"
1515
"strings"
1616

17-
"github.com/ghodss/yaml"
17+
"github.com/ghodss/yaml/v2"
1818
)
1919

2020
func foundUnresolvedRef(ref string) error {

openapi3/schema.go

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"math"
1010
"math/big"
1111
"regexp"
12+
"sort"
1213
"strconv"
1314
"unicode/utf16"
1415

@@ -150,14 +151,18 @@ type Schema struct {
150151
// Object
151152
Required []string `json:"required,omitempty" yaml:"required,omitempty"`
152153
Properties Schemas `json:"properties,omitempty" yaml:"properties,omitempty"`
154+
propertyKeys []string // order kept
153155
MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"`
154156
MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"`
155157
AdditionalPropertiesAllowed *bool `multijson:"additionalProperties,omitempty" json:"-" yaml:"-"` // In this order...
156158
AdditionalProperties *SchemaRef `multijson:"additionalProperties,omitempty" json:"-" yaml:"-"` // ...for multijson
157159
Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"`
158160
}
159161

160-
var _ jsonpointer.JSONPointable = (*Schema)(nil)
162+
var (
163+
_ jsonpointer.JSONPointable = (*Schema)(nil)
164+
_ json.Unmarshaler = (*Schema)(nil)
165+
)
161166

162167
func NewSchema() *Schema {
163168
return &Schema{}
@@ -168,7 +173,42 @@ func (schema *Schema) MarshalJSON() ([]byte, error) {
168173
}
169174

170175
func (schema *Schema) UnmarshalJSON(data []byte) error {
171-
return jsoninfo.UnmarshalStrictStruct(data, schema)
176+
if err := jsoninfo.UnmarshalStrictStruct(data, schema); err != nil {
177+
return err
178+
}
179+
180+
var rawProperties struct {
181+
Properties json.RawMessage `json:"properties"`
182+
}
183+
184+
if err := json.Unmarshal(data, &rawProperties); err != nil {
185+
return fmt.Errorf("failed to extract raw schema properties: %w", err)
186+
}
187+
188+
if schema.Type == "object" && rawProperties.Properties != nil {
189+
keys, _ := jsoninfo.ExtractObjectKeys(rawProperties.Properties)
190+
schema.propertyKeys = keys
191+
}
192+
193+
return nil
194+
}
195+
196+
// OrderedPropertyKeys returns the keys of the properties in the order they were
197+
// defined. This is useful for generating code that needs to iterate over the
198+
// properties in a consistent order. If the keys could not be extracted for some
199+
// reason, then this method automatically sorts the keys to be deterministic.
200+
func (schema Schema) OrderedPropertyKeys() []string {
201+
if schema.propertyKeys != nil {
202+
return schema.propertyKeys
203+
}
204+
205+
keys := make([]string, 0, len(schema.Properties))
206+
for k := range schema.Properties {
207+
keys = append(keys, k)
208+
}
209+
210+
sort.Strings(keys)
211+
return keys
172212
}
173213

174214
func (schema Schema) JSONLookup(token string) (interface{}, error) {

openapi3/schema_test.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,6 +1220,26 @@ components:
12201220
require.Contains(t, err.Error(), `Error at "/ownerName": Doesn't match schema "not"`)
12211221
}
12221222

1223+
func TestSchemaOrderedProperties(t *testing.T) {
1224+
const api = `
1225+
openapi: "3.0.1"
1226+
components:
1227+
schemas:
1228+
Pet:
1229+
properties:
1230+
z_name:
1231+
type: string
1232+
a_ownerName:
1233+
not:
1234+
type: boolean
1235+
type: object
1236+
`
1237+
s, err := NewLoader().LoadFromData([]byte(api))
1238+
require.NoError(t, err)
1239+
require.NotNil(t, s)
1240+
require.Equal(t, []string{"z_name", "a_ownerName"}, s.Components.Schemas["Pet"].Value.propertyKeys)
1241+
}
1242+
12231243
func TestValidationFailsOnInvalidPattern(t *testing.T) {
12241244
schema := Schema{
12251245
Pattern: "[",

0 commit comments

Comments
 (0)