Skip to content

Commit d6457d3

Browse files
committed
multiple fixes related to lenient validation:
- fix ArrayIndexOutOfBoundsException when trying to validate an empty string against a number/integer schema - handle numeric literals starting with a + sign - handle "integer" schema validation and strings representing floating point numbers
1 parent 61b9a9e commit d6457d3

File tree

4 files changed

+99
-47
lines changed

4 files changed

+99
-47
lines changed

src/main/kotlin/com/github/erosb/jsonsKema/JsonValue.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package com.github.erosb.jsonsKema
33
import org.yaml.snakeyaml.Yaml
44
import java.io.StringReader
55
import java.math.BigDecimal
6+
import java.math.BigInteger
67
import java.net.URI
78
import java.util.stream.Collectors.joining
89

@@ -289,6 +290,8 @@ data class JsonNumber @JvmOverloads constructor(
289290
if (other !is IJsonNumber) return false
290291
return BigDecimal(value.toString()).compareTo(BigDecimal(other.value.toString())) == 0
291292
}
293+
294+
internal fun isInteger() = value is Int || value is Long || value is Short || value is BigInteger || value is Byte
292295
}
293296

294297
data class JsonString @JvmOverloads constructor(

src/main/kotlin/com/github/erosb/jsonsKema/Validator.kt

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ private fun isDecimalNotation(value: String): Boolean {
2424

2525
private fun stringToNumber(value: String): Number {
2626
val initial = value.get(0)
27-
if ((initial >= '0' && initial <= '9') || initial == '-') {
27+
if ((initial >= '0' && initial <= '9') || initial == '-' || initial == '+') {
2828
// decimal representation
2929
if (isDecimalNotation(value)) {
3030
// Use a BigDecimal all the time so we keep the original
@@ -333,16 +333,28 @@ private class DefaultValidator(
333333
}
334334
}
335335
if (schema.type.value == "number" || schema.type.value == "integer") {
336-
/*
337-
* If it might be a number, try converting it. If a number cannot be
338-
* produced, then the value will just be a string.
339-
*/
340-
val initial: Char = stringInstance.get(0)
341-
if ((initial >= '0' && initial <= '9') || initial == '-') {
342-
try {
343-
instance = JsonNumber(stringToNumber(stringInstance), instance.location)
344-
return null
345-
} catch (ignore: NumberFormatException) {
336+
if (stringInstance.isNotEmpty()) {
337+
/*
338+
* If it might be a number, try converting it. If a number cannot be
339+
* produced, then the value will just be a string.
340+
*/
341+
val initial: Char = stringInstance.get(0)
342+
343+
if ((initial >= '0' && initial <= '9') || initial == '-' || initial == '+') {
344+
try {
345+
val num = JsonNumber(stringToNumber(stringInstance), instance.location)
346+
instance = num
347+
return if (schema.type.value == "integer" && !num.isInteger())
348+
TypeValidationFailure(
349+
"number",
350+
this.schema,
351+
instance,
352+
dynamicPath() + Keyword.TYPE
353+
)
354+
else null
355+
} catch (ignore: NumberFormatException) {
356+
println(ignore)
357+
}
346358
}
347359
}
348360
}

src/test/kotlin/com/github/erosb/jsonsKema/JsonParserTest.kt

Lines changed: 28 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -187,47 +187,39 @@ class JsonParserTest {
187187
assertEquals(expected, actual)
188188
}
189189

190-
// @Nested
191-
// inner class UnicodeEscapeSequenceTest {
192-
//
193-
// companion object {
194-
// @JvmStatic
195-
// fun parsers() = JsonParserTest.parsers()
196-
// }
197-
198-
@ParameterizedTest
199-
@MethodSource("parsers")
200-
fun `escaped unicode codepoint`() {
201-
val actual = JsonParser("\"\\u00E1\"")().requireString().value
202-
assertEquals("á", actual)
203-
}
190+
@ParameterizedTest
191+
@MethodSource("parsers")
192+
fun `escaped unicode codepoint`() {
193+
val actual = JsonParser("\"\\u00E1\"")().requireString().value
194+
assertEquals("á", actual)
195+
}
204196

205-
@ParameterizedTest
206-
@MethodSource("parsers")
207-
fun `invalid unicode escape - invalid hex chars`() {
208-
val exception = assertThrows(JsonParseException::class.java) {
209-
JsonParser("\"p\\u022suffix\"")()
210-
}
211-
assertEquals("invalid unicode sequence: 022s", exception.message)
212-
assertEquals(TextLocation(1, 4, DEFAULT_BASE_URI), exception.location)
197+
@ParameterizedTest
198+
@MethodSource("parsers")
199+
fun `invalid unicode escape - invalid hex chars`() {
200+
val exception = assertThrows(JsonParseException::class.java) {
201+
JsonParser("\"p\\u022suffix\"")()
213202
}
203+
assertEquals("invalid unicode sequence: 022s", exception.message)
204+
assertEquals(TextLocation(1, 4, DEFAULT_BASE_URI), exception.location)
205+
}
214206

215-
@ParameterizedTest
216-
@MethodSource("parsers")
217-
fun `invalid unicode escape - not enough hex chars`() {
218-
val exception = assertThrows(JsonParseException::class.java) {
219-
JsonParser("\"p\\u022")()
220-
}
221-
assertEquals("Unexpected EOF", exception.message)
222-
assertEquals(TextLocation(1, 8, DEFAULT_BASE_URI), exception.location)
207+
@ParameterizedTest
208+
@MethodSource("parsers")
209+
fun `invalid unicode escape - not enough hex chars`() {
210+
val exception = assertThrows(JsonParseException::class.java) {
211+
JsonParser("\"p\\u022")()
223212
}
213+
assertEquals("Unexpected EOF", exception.message)
214+
assertEquals(TextLocation(1, 8, DEFAULT_BASE_URI), exception.location)
215+
}
224216

225-
@ParameterizedTest
226-
@MethodSource("parsers")
227-
fun `supplementary codepoint`() {
228-
val str = JsonParser("\"\\uD83D\\uDCA9\"")().requireString().value
229-
assertEquals("💩", str)
230-
}
217+
@ParameterizedTest
218+
@MethodSource("parsers")
219+
fun `supplementary codepoint`() {
220+
val str = JsonParser("\"\\uD83D\\uDCA9\"")().requireString().value
221+
assertEquals("💩", str)
222+
}
231223
// }
232224

233225
@ParameterizedTest

src/test/kotlin/com/github/erosb/jsonsKema/LenientModeTest.kt

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,51 @@ class LenientModeTest {
131131
assertThat(actual!!.causes).hasSize(2)
132132
}
133133

134+
@Test
135+
fun `expected integer, actual positive fractional string`() {
136+
val schema = SchemaLoader("""
137+
{
138+
"type": "integer",
139+
"minimum": 5,
140+
"maximum": 3
141+
}
142+
""".trimIndent())()
143+
144+
val rawActual = Validator.create(
145+
schema, ValidatorConfig(primitiveValidationStrategy = PrimitiveValidationStrategy.LENIENT)
146+
).validate(
147+
"""
148+
"+4.4"
149+
""".trimIndent()
150+
)!!
151+
println(rawActual)
152+
assertThat(rawActual.causes.filterIsInstance<MinimumValidationFailure>()).hasSize(1)
153+
assertThat(rawActual.causes.filterIsInstance<MaximumValidationFailure>()).hasSize(1)
154+
assertThat(rawActual.causes.filterIsInstance<TypeValidationFailure>().single().message).isEqualTo("expected type: integer, actual: number")
155+
}
156+
157+
@Test
158+
fun `expected integer, actual empty string`() {
159+
val schema = SchemaLoader("""
160+
{
161+
"type": "integer",
162+
"minimum": 5,
163+
"maximum": 3
164+
}
165+
""".trimIndent())()
166+
167+
val actual = Validator.create(
168+
schema, ValidatorConfig(
169+
primitiveValidationStrategy = PrimitiveValidationStrategy.LENIENT
170+
)
171+
).validate(
172+
"""
173+
""
174+
""".trimIndent()
175+
)
176+
assertThat(actual).isInstanceOf(TypeValidationFailure::class.java)
177+
}
178+
134179
@Test
135180
fun `optional properties can be null`() {
136181
val schema = SchemaLoader("""

0 commit comments

Comments
 (0)