amazon-ion / ion-schema-kotlin

A Kotlin reference implementation of the Ion Schema Specification.
https://amazon-ion.github.io/ion-schema/
Apache License 2.0
26 stars 14 forks source link

StackOverflow when validating 'null.string' but not `null`. #284

Closed dlurton closed 1 year ago

dlurton commented 1 year ago

This might be a duplicate of https://github.com/amazon-ion/ion-schema-kotlin/issues/169 but I thought I'd file an issue anyway since the situation appears to be slightly different (I'm not specifying any specific Ion or core type here)... I discovered this while attempting to place a constraint on a nullable field that only applied when the value was not null.

The first call to validate() print indicates "null" is not valid for the bat type (unexpected--the type is nullable::, the second call (different only by the typedness of the null value) results in a stack overlfow.

import com.amazon.ion.system.IonSystemBuilder
import com.amazon.ionschema.IonSchemaSystemBuilder
import com.amazon.ionschema.Schema
import org.junit.jupiter.api.Test

class Test {
    val ion = IonSystemBuilder.standard().build()

    @Test
    fun reproduce() {
        val iss = IonSchemaSystemBuilder.standard().withIonSystem(ion).build()

        val schema = iss.newSchema("""
            type::{
                name: bat,
                any_of: [
                 nullable::{valid_values: ["a"]}
                ]
            }
        """.trimIndent())

        validate(schema, "null")
        validate(schema, "null.string")
    }

    private fun validate(schema: Schema, inputText: String) {
        val input = ion.singleValue(inputText)

        val type = schema.getType("bat")!!

        val violations = type.validate(input)
        println("isValid: ${violations.isValid()}")
        println(violations)
    }
}

Output:

isValid: false
Validation failed:
- expected type any
  - value matches none of the types
    - expected type blob, found null
    - expected type bool, found null
    - expected type clob, found null
    - expected type decimal, found null
    - expected type document, found null
    - expected type float, found null
    - expected type int, found null
    - expected type string, found null
    - expected type symbol, found null
    - expected type timestamp, found null
    - expected type list, found null
    - expected type sexp, found null
    - expected type struct, found null

java.lang.StackOverflowError
    at com.amazon.ion.impl.lite.IonStructLite.find_field_helper(IonStructLite.java:431)
    at com.amazon.ion.impl.lite.IonStructLite.get(IonStructLite.java:402)
    at com.amazon.ionschema.internal.TypeImpl.getBaseType(TypeImpl.kt:73)
    at com.amazon.ionschema.internal.TypeImpl.isValidForBaseType(TypeImpl.kt:84)
    at com.amazon.ionschema.internal.TypeBuiltinImpl.isValidForBaseType(TypeBuiltinImpl.kt)
    at com.amazon.ionschema.internal.TypeImpl.isValidForBaseType(TypeImpl.kt:84)

... and so on...
popematt commented 1 year ago

You're right that it is both very close to the other issue, but it is slightly different. The other issue was meant to be more illustrative of the problems with nullable:: in general. I think the root cause it the same though.

There are some types for this Ion Schema Kotlin is unable to determine a "base type" to use for the purpose of allowing a typed null. In Ion Schema Kotlin, this results in a SO Error because it naively tries to find the base type of e.g. any (which is a union of all types) and so there is no single Ion type that is the "base type" for any.

You can deal with this in a few different of ways. One would be to make the type explicit in the nullable::-annotated type definition. E.g.:

type::{
  name: bat,
  any_of: [
    nullable::{type:string, valid_values: ["a"]}
  ]
}

If you are specifically dealing with an inline type that has only a valid_values constraint, you can simply add the desired null values to the list instead of using nullable::. E.g.:

type::{
  name: bat,
  any_of: [
   {valid_values: ["a", null, null.string]}
  ]
}

Alternately, you can use Ion Schema 2.0, which drops nullable:: (because it was confusing, and it's sometimes not possible to find the correct "base type") in favor of $null_or:: which is syntactical sugar for the union of the decorated type and the $null type.

Do any of those solutions work for you?