Skip to content

Commit 9187b22

Browse files
committed
Add openapi3.Schema.PropertyKeys for deterministic order
This commit adds the `PropertyKeys` property to the type `openapi3.Schema` which contains the keys of the `Properties` map in the order that they appear in the original YAML file. This is useful to guarantee deterministic code generation. 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 9187b22

File tree

6 files changed

+96
-2
lines changed

6 files changed

+96
-2
lines changed

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: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ type Schema struct {
127127
// Object
128128
Required []string `json:"required,omitempty" yaml:"required,omitempty"`
129129
Properties Schemas `json:"properties,omitempty" yaml:"properties,omitempty"`
130+
PropertyKeys []string `json:"-" yaml:"-"` // deterministically ordered keys
130131
MinProps uint64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"`
131132
MaxProps *uint64 `json:"maxProperties,omitempty" yaml:"maxProperties,omitempty"`
132133
AdditionalProperties AdditionalProperties `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"`
@@ -410,6 +411,23 @@ func (schema *Schema) UnmarshalJSON(data []byte) error {
410411
if err := json.Unmarshal(data, &x); err != nil {
411412
return unmarshalError(err)
412413
}
414+
415+
if x.Properties != nil {
416+
var rawProperties struct {
417+
Properties json.RawMessage `json:"properties"`
418+
}
419+
if err := json.Unmarshal(data, &rawProperties); err != nil {
420+
// Straight up panic because UnmarshalJSON should already guarantee
421+
// a valid input.
422+
panic(err)
423+
}
424+
k, err := extractObjectKeys(rawProperties.Properties)
425+
if err != nil {
426+
panic(err)
427+
}
428+
x.PropertyKeys = k
429+
}
430+
413431
_ = json.Unmarshal(data, &x.Extensions)
414432

415433
delete(x.Extensions, "oneOf")

openapi3/schema_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1490,3 +1490,26 @@ 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+
}

0 commit comments

Comments
 (0)