OptimumCode/json-schema-validator

Register custom Keywords

Closed this issue · 4 comments

It would be great to be able to register custom keywords.

Playing around with this library I was able to easily create a custom keyword, but I have no way to register it.

Thanks!

Hi @colbyharrison,

Thank you for your interest in the library and your feedback!

Yes, creating custom keywords is quite simple because the factory interface and keyword interface have a small API exposed.

However, the library is in the stage where it supports only Draft 7 and does not yet support Draft 2019-09 (Draft 8) and 2020-12. I have started work on adding Draft 2019-09 but it is moving quite slowly, unfortunately. And the API for factory and keywords will evolve (I am not sure yet how it is going to look in the end). Because of that, I would prefer to keep this API internal until both Draft 2019-09 and 2020-12 are implemented. Otherwise, the library will have to either keep the backward compatibility for that API (that I don't want at the current stage) or the user will have to migrate the custom keywords they created once the API is changed (that is not good either).

Once Draft 2019-09 and 2020-12 are done, I would consider exposing this API to the user and allowing the registration of the custom keywords. It might be a good feature for the library.

If it is not a secret could you please add some detail about why you need to register custom keywords? It would be interesting to hear your use case.

Thanks for the quick reply.

I see your point about the api still evolving. I might argue that allowing for customization gives potential users more reason to use this library, as they can fill the current gaps of this library themselves. I think the benefit of a pure Kotlin common implementation is worth the cost of changing api.

Regardless, it's no secret. I have a requirement to support the format Keyword, specifically for Date and Time (https://json-schema.org/draft/2020-12/draft-bhutton-json-schema-validation-00#rfc.section.7.3.1). On top of that I need to support an extension of Json Schema (FormatMaximum, FormatMinimum, FormatExclusiveMaximum, FormatExclusiveMinimum) .

I've implemented these things on my end and was pretty impressed with how easy it was to do, even with the odd requirement that the formatMaximum type keywords have a dependency on the previous format keyword to know the context of the comparison.

Here's what the base class of that AssertionFactory looks like:

internal abstract class FormatXAssertionFactory(formatProperty: String) :
    AbstractAssertionFactory(formatProperty) {
    protected lateinit var formatKeyword: FormatKeyword
    
    override fun create(element: JsonElement, context: LoadingContext): JsonSchemaAssertion {
        require(element is JsonObject)
        val formatElement =
            element.getOrElse("format"/* todo directly access the property of another Assertion*/) {
                throw IllegalArgumentException("Format Dependency not included")
            }
        require(formatElement is JsonPrimitive)
        formatKeyword = formatElement.content.toFormatKeyword()
        return super.create(element, context)
    }

    override fun isApplicable(element: JsonElement): Boolean {
        return super.isApplicable(element) && FormatAssertionFactory.isApplicable(element)
    }
}

I'd really like to see this library flourish, and would be happy to contribute to it if you feel it's ready for that.

Thank you, @colbyharrison, for sharing the use case.
Once I add the remaining drafts I will think about how to better include this functionality in the library API.

By the way, I would like to suggest some changes in the implementation of the new keywords:

  • Assertions and factories should not depend on other keywords directly (at least this is what was in my head when I decided to create this library). For example, if-else-then keywords. They depend on each other, however, neither the assertion factory nor the assertion implementation itself knows about other keywords (except the annotations other keywords might provide)
  • There is an annotation concept. Each assertion can add an annotation to the object it currently validates:
    internal interface AssertionContext {
      fun <T : Any> annotate(key: AnnotationKey<T>, value: T)
      fun <T : Any> annotated(key: AnnotationKey<T>): T?
    }
    In your case, the format keyword could annotate the object with a format it expects (if the object matches this format).
    The format* keyword could get this annotation and perform the assertion according to the format the object has or ignore the object if there is no annotation from the format keyword

Ah I missed that. Thank you @OptimumCode thats a better solution.