Skip to content

Commit 1c48de3

Browse files
json: ImpliedType rejects duplicate property names of different types
1 parent d13b46e commit 1c48de3

File tree

3 files changed

+71
-0
lines changed

3 files changed

+71
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# 1.16.2 (Unreleased)
22

3+
* `json`: `ImpliedType` now returns an error if a JSON object contains two properties of the same name. As a compatibility concession it allows duplicates whose values have the same implied type, since it was unintentionally possible to combine `ImpliedType` and `Unmarshal` successfully in that case before, but this is not an endorsement of using duplicate property names since that makes the input ambiguous in any case. ([#199](https://github.com/zclconf/go-cty/issues/199))
34
* `function/stdlib`: `ElementFunc` no longer crashes when asked for a negative index into a tuple. This fixes a miss in the negative index support added back in v1.15.0. ([#200](https://github.com/zclconf/go-cty/pull/200))
45

56
# 1.16.1 (January 13, 2025)

cty/json/type_implied.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,29 @@ func impliedObjectType(dec *json.Decoder) (cty.Type, error) {
127127
if atys == nil {
128128
atys = make(map[string]cty.Type)
129129
}
130+
if existing, exists := atys[key]; exists {
131+
// We didn't originally have any special treatment for multiple properties
132+
// of the same name, having the type of the last one "win". But that caused
133+
// some confusing error messages when the same input was subsequently used
134+
// with [Unmarshal] using the returned object type, since [Unmarshal] would
135+
// try to fit all of the property values of that name to whatever type
136+
// the last one had, and would likely fail in doing so if the earlier
137+
// properties of the same name had different types.
138+
//
139+
// As a compromise to avoid breaking existing successful use of _consistently-typed_
140+
// redundant properties, we return an error here only if the new type
141+
// differs from the old type. The error message doesn't mention that subtlety
142+
// because the equal type carveout is a compatibility concession rather than
143+
// a feature folks should rely on in new code.
144+
if !existing.Equals(aty) {
145+
// This error message is low-quality because ImpliedType doesn't do
146+
// path tracking while it traverses, unlike Unmarshal. However, this
147+
// is a rare enough case that we don't want to pay the cost of allocating
148+
// another path-tracking buffer that would in most cases be ignored,
149+
// so we just accept a low-context error message. :(
150+
return cty.NilType, fmt.Errorf("duplicate %q property in JSON object", key)
151+
}
152+
}
130153
atys[key] = aty
131154
}
132155

cty/json/type_implied_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,12 @@ func TestImpliedType(t *testing.T) {
9191
}),
9292
}),
9393
},
94+
{
95+
`{"a": "hello", "a": "world"}`,
96+
cty.Object(map[string]cty.Type{
97+
"a": cty.String,
98+
}),
99+
},
94100
}
95101

96102
for _, test := range tests {
@@ -110,3 +116,44 @@ func TestImpliedType(t *testing.T) {
110116
})
111117
}
112118
}
119+
120+
func TestImpliedTypeErrors(t *testing.T) {
121+
tests := []struct {
122+
Input string
123+
WantError string
124+
}{
125+
{
126+
`{"a": "hello", "a": true}`,
127+
`duplicate "a" property in JSON object`,
128+
},
129+
{
130+
`{}boop`,
131+
`extraneous data after JSON object`,
132+
},
133+
{
134+
`[!]`,
135+
`invalid character '!' looking for beginning of value`,
136+
},
137+
{
138+
`[}`,
139+
`invalid character '}' looking for beginning of value`,
140+
},
141+
{
142+
`{true: null}`,
143+
`invalid character 't'`,
144+
},
145+
}
146+
147+
for _, test := range tests {
148+
t.Run(test.Input, func(t *testing.T) {
149+
_, err := ImpliedType([]byte(test.Input))
150+
if err == nil {
151+
t.Fatalf("unexpected success\nwant error: %s", err)
152+
}
153+
154+
if got, want := err.Error(), test.WantError; got != want {
155+
t.Errorf("wrong error\ngot: %s\nwant: %s", got, want)
156+
}
157+
})
158+
}
159+
}

0 commit comments

Comments
 (0)