Skip to content

Commit 5316d46

Browse files
committed
Handle JSON null when reading object/array-typed properties
JSONSchemaObject>>readUsing: and JSONSchemaArray>>readUsing: went straight to a structural parse (parseMapKeysDo:/parseListDo:, expecting { or [ respectively) with no concept of null, even though any property can legitimately be JSON null regardless of its declared type - Stripe's responses do this constantly (address, discount, items, and most other optional fields are null unless set). Added a shared JSONSchema>>peekNullThenReadUsing:ifNotNull: (uses parseConstantDo:, which does not consume the stream on a non-match, to peek for null/true/false before committing to the structural read) and wired both readUsing: methods through it. A literal true/false where an object/array was expected now raises a clear JSONTypeError instead of an opaque NeoJSONParseError ('{ expected'/'[ expected'). Found the same way as the rest of today's fixes: driving OpenApiClient against the real Stripe spec + a live stripe-mock instance. The full Customer and Subscription response bodies - both deeply nested, both full of null fields - now read back correctly end to end.
1 parent be9a7aa commit 5316d46

4 files changed

Lines changed: 79 additions & 28 deletions

File tree

‎source/JSONSchema-Core-Tests/JSONSchemaTests.class.st‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,31 @@ JSONSchemaTests >> testSchemaReadArrayMultipleItems [
5757
self assert: object foo third equals: (DateAndTime year: 2019 month: 10 day: 9 hour: 13 minute: 15 second: 8 offset: 1 hour).
5858
]
5959

60+
{ #category : 'tests' }
61+
JSONSchemaTests >> testSchemaReadArrayPropertyThatIsNull [
62+
"A schema saying a property is an array does not forbid the actual
63+
value being JSON null - readString: must not crash trying to parse
64+
null as a list."
65+
| schema object |
66+
schema := {
67+
#foo -> (JSONSchemaArray new items: JSONSchema string).
68+
} asJSONSchema.
69+
object := schema readString: '{"foo":null}'.
70+
self assert: object foo equals: nil
71+
]
72+
73+
{ #category : 'tests' }
74+
JSONSchemaTests >> testSchemaReadArrayPropertyThatIsWrongType [
75+
"A literal true/false where an array was expected is invalid JSON for
76+
this schema and should raise a clear type error, not an obscure parser
77+
error from trying to read it as a list."
78+
| schema |
79+
schema := {
80+
#foo -> (JSONSchemaArray new items: JSONSchema string).
81+
} asJSONSchema.
82+
self should: [ schema readString: '{"foo":true}' ] raise: JSONTypeError
83+
]
84+
6085
{ #category : 'tests' }
6186
JSONSchemaTests >> testSchemaReadNested [
6287

@@ -75,6 +100,17 @@ JSONSchemaTests >> testSchemaReadNested [
75100

76101
]
77102

103+
{ #category : 'tests' }
104+
JSONSchemaTests >> testSchemaReadNestedObjectPropertyThatIsNull [
105+
"Same as above for an object-typed property."
106+
| schema object |
107+
schema := {
108+
#bar -> { #one -> JSONSchema string }
109+
} asJSONSchema.
110+
object := schema readString: '{"bar":null}'.
111+
self assert: object bar equals: nil
112+
]
113+
78114
{ #category : 'tests' }
79115
JSONSchemaTests >> testSchemaWriteArray [
80116

‎source/JSONSchema-Core/JSONSchema.class.st‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,24 @@ JSONSchema >> neoJsonOn: jsonWriter [
164164

165165
]
166166

167+
{ #category : 'private' }
168+
JSONSchema >> peekNullThenReadUsing: aReader ifNotNull: aBlock [
169+
"Both object- and array-typed schemas hand off straight to a structural
170+
parse (parseMapKeysDo:/parseListDo:), which expects { or [ and has no
171+
concept of null - but any OpenAPI/JSON-Schema property can legitimately
172+
be null regardless of its declared type. Peek for a null/true/false
173+
literal first (parseConstantDo: does not consume the stream if there is
174+
no match) before committing to the structural read."
175+
| matched value |
176+
matched := false.
177+
value := nil.
178+
aReader parseConstantDo: [ :v | matched := true. value := v ].
179+
(matched and: [ value isNil ]) ifTrue: [ ^ nil ].
180+
matched ifTrue: [
181+
^ JSONTypeError signal: self class typeName, ' cannot be ', value printString ].
182+
^ aBlock value
183+
]
184+
167185
{ #category : 'instance creation' }
168186
JSONSchema >> readFrom: aStream [
169187
^ self readUsing: (NeoJSONReader on: aStream)

‎source/JSONSchema-Core/JSONSchemaArray.class.st‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,15 +61,16 @@ JSONSchemaArray >> read: aString object: object [
6161

6262
{ #category : 'private' }
6363
JSONSchemaArray >> readUsing: aReader [
64-
| index |
65-
index := 0.
66-
^ Array streamContents: [:stream |
64+
^ self peekNullThenReadUsing: aReader ifNotNull: [
65+
| index |
66+
index := 0.
67+
Array streamContents: [:stream |
6768
aReader parseListDo: [
6869
stream nextPut: (items isCollection
6970
ifTrue: [
7071
index := index + 1.
7172
(items at: index) readUsing: aReader ]
72-
ifFalse: [ items readUsing: aReader ])]]
73+
ifFalse: [ items readUsing: aReader ])]] ]
7374
]
7475

7576
{ #category : 'accessing' }

‎source/JSONSchema-Core/JSONSchemaObject.class.st‎

Lines changed: 20 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -149,27 +149,24 @@ JSONSchemaObject >> read: string object: object [
149149
]
150150

151151
{ #category : 'as yet unclassified' }
152-
JSONSchemaObject >> readKey: key reader: reader [
153-
properties ifNotNil: [
154-
properties
155-
at: key
156-
ifPresent: [ :propertySchema |
157-
"schema for property is existing using it to read the value"
158-
^ propertySchema readUsing: reader ] ].
159-
160-
(additionalProperties isNil | (additionalProperties = true))
161-
ifTrue: [
162-
"if additionProperties is absent or true any additional property
163-
is allowed"
164-
^ reader parseValue ].
165-
166-
(additionalProperties = false)
167-
ifTrue: [
168-
"addtional properties are disallowed"
169-
JSONTypeError signal: 'additional properties are not allowed' ].
170-
171-
"if additionalProperty is present and not a boolean it is a
172-
schema defining the type of the addtional properties"
152+
JSONSchemaObject >> readKey: key reader: reader [
153+
properties ifNotNil: [
154+
properties
155+
at: key
156+
ifPresent: [ :propertySchema |
157+
"schema for property is existing using it to read the value"
158+
^ propertySchema readUsing: reader ] ].
159+
(additionalProperties isNil | (additionalProperties = true))
160+
ifTrue: [
161+
"if additionProperties is absent or true any additional property
162+
is allowed"
163+
^ reader parseValue ].
164+
(additionalProperties = false)
165+
ifTrue: [
166+
"addtional properties are disallowed"
167+
JSONTypeError signal: 'additional properties are not allowed' ].
168+
"if additionalProperty is present and not a boolean it is a
169+
schema defining the type of the addtional properties"
173170
^ additionalProperties readUsing: reader
174171
]
175172

@@ -194,9 +191,8 @@ JSONSchemaObject >> readString: aString [
194191

195192
{ #category : 'private' }
196193
JSONSchemaObject >> readUsing: reader [
197-
^ self
198-
readUsing: reader
199-
object: self newInstance
194+
^ self peekNullThenReadUsing: reader ifNotNull: [
195+
self readUsing: reader object: self newInstance ]
200196
]
201197

202198
{ #category : 'private' }

0 commit comments

Comments
 (0)