Skip to content

Commit d64ca8a

Browse files
committed
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 invopop/yaml#13 is merged.
1 parent af90e9a commit d64ca8a

6 files changed

Lines changed: 112 additions & 2 deletions

File tree

go.mod

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

33
go 1.20
44

5+
replace github.com/invopop/yaml => github.com/diamondburned/invopop-yaml v0.3.2-0.20240812084936-33aae275be98
6+
57
require (
68
github.com/go-openapi/jsonpointer v0.21.0
79
github.com/gorilla/mux v1.8.0

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
22
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
3+
github.com/diamondburned/invopop-yaml v0.3.2-0.20240812084936-33aae275be98 h1:Z+YbYkmppSq0GA/nr1d8+VVf3UpALBQSyFYixw7gb44=
4+
github.com/diamondburned/invopop-yaml v0.3.2-0.20240812084936-33aae275be98/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
35
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
46
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
57
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
68
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
79
github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM=
810
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
911
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
10-
github.com/invopop/yaml v0.3.1 h1:f0+ZpmhfBSS4MhG+4HYseMdJhoeeopbSKbq5Rpeelso=
11-
github.com/invopop/yaml v0.3.1/go.mod h1:PMOp3nn4/12yEZUFfmOuNHJsZToEEOwoWsT+D81KkeA=
1212
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
1313
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
1414
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=

openapi3/marsh.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package openapi3
22

33
import (
4+
"bytes"
45
"encoding/json"
56
"fmt"
7+
"log"
68
"strings"
79

810
"github.com/invopop/yaml"
@@ -32,3 +34,41 @@ func unmarshal(data []byte, v any) error {
3234
// If both unmarshaling attempts fail, return a new error that includes both errors
3335
return fmt.Errorf("failed to unmarshal data: json error: %v, yaml error: %v", jsonErr, yamlErr)
3436
}
37+
38+
// extractObjectKeys extracts the keys of an object in a JSON string. The keys
39+
// are returned in the order they appear in the JSON string.
40+
func extractObjectKeys(b []byte) ([]string, error) {
41+
if !bytes.HasPrefix(b, []byte{'{'}) {
42+
return nil, fmt.Errorf("expected '{' at start of JSON object")
43+
}
44+
45+
dec := json.NewDecoder(bytes.NewReader(b))
46+
var keys []string
47+
48+
for dec.More() {
49+
// Read prop name
50+
t, err := dec.Token()
51+
if err != nil {
52+
log.Printf("Err: %v", err)
53+
break
54+
}
55+
56+
name, ok := t.(string)
57+
if !ok {
58+
continue // May be a delimeter
59+
}
60+
61+
keys = append(keys, name)
62+
63+
var whatever nullMessage
64+
dec.Decode(&whatever)
65+
}
66+
67+
return keys, nil
68+
}
69+
70+
// nullMessage implements json.Unmarshaler and does nothing with the given
71+
// value.
72+
type nullMessage struct{}
73+
74+
func (*nullMessage) UnmarshalJSON(data []byte) error { return nil }

openapi3/marsh_test.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,14 @@ paths:
7676
err = doc.Validate(sl.Context)
7777
require.NoError(t, err)
7878
}
79+
80+
func TestExtractObjectKeys(t *testing.T) {
81+
const j = `{
82+
"z_hello": "world",
83+
"a_foo": "bar",
84+
}`
85+
86+
keys, err := extractObjectKeys([]byte(j))
87+
require.NoError(t, err)
88+
require.Equal(t, []string{"z_hello", "a_foo"}, keys)
89+
}

openapi3/schema.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,8 @@ type Schema struct {
131131
MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"`
132132
AdditionalProperties AdditionalProperties `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"`
133133
Discriminator *Discriminator `json:"discriminator,omitempty" yaml:"discriminator,omitempty"`
134+
// list of keys in Properties with preserved order
135+
propertyKeys []string
134136
}
135137

136138
type Types []string
@@ -410,6 +412,16 @@ func (schema *Schema) UnmarshalJSON(data []byte) error {
410412
if err := json.Unmarshal(data, &x); err != nil {
411413
return unmarshalError(err)
412414
}
415+
416+
if x.Properties != nil {
417+
var rawProperties struct {
418+
Properties json.RawMessage `json:"properties"`
419+
}
420+
if err := json.Unmarshal(data, &rawProperties); err == nil {
421+
x.propertyKeys, _ = extractObjectKeys(rawProperties.Properties)
422+
}
423+
}
424+
413425
_ = json.Unmarshal(data, &x.Extensions)
414426

415427
delete(x.Extensions, "oneOf")
@@ -476,6 +488,24 @@ func (schema *Schema) UnmarshalJSON(data []byte) error {
476488
return nil
477489
}
478490

491+
// OrderedPropertyKeys returns the keys of the properties in the order they were
492+
// defined. This is useful for generating code that needs to iterate over the
493+
// properties in a consistent order. If the keys could not be extracted for some
494+
// reason, then this method automatically sorts the keys to be deterministic.
495+
func (schema Schema) OrderedPropertyKeys() []string {
496+
if schema.propertyKeys != nil {
497+
return schema.propertyKeys
498+
}
499+
500+
keys := make([]string, 0, len(schema.Properties))
501+
for k := range schema.Properties {
502+
keys = append(keys, k)
503+
}
504+
505+
sort.Strings(keys)
506+
return keys
507+
}
508+
479509
// JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable
480510
func (schema Schema) JSONLookup(token string) (any, error) {
481511
switch token {

openapi3/schema_test.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1490,3 +1490,30 @@ func TestIssue751(t *testing.T) {
14901490
require.NoError(t, schema.VisitJSON(validData))
14911491
require.ErrorContains(t, schema.VisitJSON(invalidData), "duplicate items found")
14921492
}
1493+
1494+
func TestSchemaOrderedProperties(t *testing.T) {
1495+
const api = `
1496+
openapi: "3.0.1"
1497+
components:
1498+
schemas:
1499+
Pet:
1500+
properties:
1501+
z_name:
1502+
type: string
1503+
description: Diamond
1504+
a_ownerName:
1505+
not:
1506+
type: boolean
1507+
type: object
1508+
`
1509+
s, err := NewLoader().LoadFromData([]byte(api))
1510+
require.NoError(t, err)
1511+
require.NotNil(t, s)
1512+
1513+
pet := s.Components.Schemas["Pet"].Value
1514+
require.Equal(t, []string{"z_name", "a_ownerName"}, pet.propertyKeys)
1515+
require.Equal(t, []string{"z_name", "a_ownerName"}, pet.OrderedPropertyKeys())
1516+
1517+
pet.propertyKeys = nil
1518+
require.Equal(t, []string{"a_ownerName", "z_name"}, pet.OrderedPropertyKeys())
1519+
}

0 commit comments

Comments
 (0)