Skip to content

Add support for draft 6 #138

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions api/json-schema-validator.api
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ public final class io/github/optimumcode/json/schema/SchemaType : java/lang/Enum
public static final field Companion Lio/github/optimumcode/json/schema/SchemaType$Companion;
public static final field DRAFT_2019_09 Lio/github/optimumcode/json/schema/SchemaType;
public static final field DRAFT_2020_12 Lio/github/optimumcode/json/schema/SchemaType;
public static final field DRAFT_6 Lio/github/optimumcode/json/schema/SchemaType;
public static final field DRAFT_7 Lio/github/optimumcode/json/schema/SchemaType;
public static final fun find (Ljava/lang/String;)Lio/github/optimumcode/json/schema/SchemaType;
public static fun getEntries ()Lkotlin/enums/EnumEntries;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ package io.github.optimumcode.json.schema
import com.eygraber.uri.Uri
import io.github.optimumcode.json.schema.SchemaType.DRAFT_2019_09
import io.github.optimumcode.json.schema.SchemaType.DRAFT_2020_12
import io.github.optimumcode.json.schema.SchemaType.DRAFT_6
import io.github.optimumcode.json.schema.SchemaType.DRAFT_7
import io.github.optimumcode.json.schema.extension.ExternalAssertionFactory
import io.github.optimumcode.json.schema.internal.SchemaLoader
import io.github.optimumcode.json.schema.internal.wellknown.Draft201909
import io.github.optimumcode.json.schema.internal.wellknown.Draft202012
import io.github.optimumcode.json.schema.internal.wellknown.Draft6
import io.github.optimumcode.json.schema.internal.wellknown.Draft7
import kotlinx.serialization.json.JsonElement
import kotlin.jvm.JvmStatic
Expand All @@ -17,6 +19,7 @@ public interface JsonSchemaLoader {
public fun registerWellKnown(draft: SchemaType): JsonSchemaLoader =
apply {
when (draft) {
DRAFT_6 -> Draft6.entries.forEach { register(it.content) }
DRAFT_7 -> Draft7.entries.forEach { register(it.content) }
DRAFT_2019_09 -> Draft201909.entries.forEach { register(it.content) }
DRAFT_2020_12 -> Draft202012.entries.forEach { register(it.content) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ import com.eygraber.uri.Uri
import io.github.optimumcode.json.schema.internal.SchemaLoaderConfig
import io.github.optimumcode.json.schema.internal.config.Draft201909SchemaLoaderConfig
import io.github.optimumcode.json.schema.internal.config.Draft202012SchemaLoaderConfig
import io.github.optimumcode.json.schema.internal.config.Draft6SchemaLoaderConfig
import io.github.optimumcode.json.schema.internal.config.Draft7SchemaLoaderConfig
import kotlin.jvm.JvmStatic

public enum class SchemaType(
internal val schemaId: Uri,
internal val config: SchemaLoaderConfig,
) {
DRAFT_6(Uri.parse("http://json-schema.org/draft-06/schema"), Draft6SchemaLoaderConfig),
DRAFT_7(Uri.parse("http://json-schema.org/draft-07/schema"), Draft7SchemaLoaderConfig),
DRAFT_2019_09(Uri.parse("https://json-schema.org/draft/2019-09/schema"), Draft201909SchemaLoaderConfig),
DRAFT_2020_12(Uri.parse("https://json-schema.org/draft/2020-12/schema"), Draft202012SchemaLoaderConfig),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package io.github.optimumcode.json.schema.internal.config

import io.github.optimumcode.json.schema.FormatBehavior
import io.github.optimumcode.json.schema.SchemaOption
import io.github.optimumcode.json.schema.internal.AssertionFactory
import io.github.optimumcode.json.schema.internal.KeyWord
import io.github.optimumcode.json.schema.internal.KeyWord.ANCHOR
import io.github.optimumcode.json.schema.internal.KeyWord.COMPATIBILITY_DEFINITIONS
import io.github.optimumcode.json.schema.internal.KeyWord.DEFINITIONS
import io.github.optimumcode.json.schema.internal.KeyWord.DYNAMIC_ANCHOR
import io.github.optimumcode.json.schema.internal.KeyWord.ID
import io.github.optimumcode.json.schema.internal.KeyWordResolver
import io.github.optimumcode.json.schema.internal.ReferenceFactory
import io.github.optimumcode.json.schema.internal.ReferenceFactory.RefHolder
import io.github.optimumcode.json.schema.internal.SchemaLoaderConfig
import io.github.optimumcode.json.schema.internal.SchemaLoaderContext
import io.github.optimumcode.json.schema.internal.config.Draft6KeyWordResolver.REF_PROPERTY
import io.github.optimumcode.json.schema.internal.factories.array.AdditionalItemsAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.array.ContainsAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.array.ItemsAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.array.MaxItemsAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.array.MinItemsAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.array.UniqueItemsAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.condition.AllOfAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.condition.AnyOfAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.condition.NotAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.condition.OneOfAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.general.ConstAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.general.EnumAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.general.FormatAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.general.TypeAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.number.ExclusiveMaximumAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.number.ExclusiveMinimumAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.number.MaximumAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.number.MinimumAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.number.MultipleOfAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.`object`.AdditionalPropertiesAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.`object`.DependenciesAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.`object`.MaxPropertiesAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.`object`.MinPropertiesAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.`object`.PatternPropertiesAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.`object`.PropertiesAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.`object`.PropertyNamesAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.`object`.RequiredAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.string.MaxLengthAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.string.MinLengthAssertionFactory
import io.github.optimumcode.json.schema.internal.factories.string.PatternAssertionFactory
import io.github.optimumcode.json.schema.internal.util.getStringRequired
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject

internal object Draft6SchemaLoaderConfig : SchemaLoaderConfig {
private val factories: List<AssertionFactory> =
listOf(
TypeAssertionFactory,
EnumAssertionFactory,
ConstAssertionFactory,
MultipleOfAssertionFactory,
MaximumAssertionFactory,
ExclusiveMaximumAssertionFactory,
MinimumAssertionFactory,
ExclusiveMinimumAssertionFactory,
MaxLengthAssertionFactory,
MinLengthAssertionFactory,
PatternAssertionFactory,
ItemsAssertionFactory,
AdditionalItemsAssertionFactory,
MaxItemsAssertionFactory,
MinItemsAssertionFactory,
UniqueItemsAssertionFactory,
ContainsAssertionFactory,
MaxPropertiesAssertionFactory,
MinPropertiesAssertionFactory,
RequiredAssertionFactory,
PropertiesAssertionFactory,
PatternPropertiesAssertionFactory,
AdditionalPropertiesAssertionFactory,
PropertyNamesAssertionFactory,
DependenciesAssertionFactory,
AllOfAssertionFactory,
AnyOfAssertionFactory,
OneOfAssertionFactory,
NotAssertionFactory,
)

override val defaultVocabulary: SchemaLoaderConfig.Vocabulary = SchemaLoaderConfig.Vocabulary()
override val allFactories: List<AssertionFactory>
get() = factories

override fun createVocabulary(schemaDefinition: JsonElement): SchemaLoaderConfig.Vocabulary? = null

override fun factories(
schemaDefinition: JsonElement,
vocabulary: SchemaLoaderConfig.Vocabulary,
options: SchemaLoaderConfig.Options,
): List<AssertionFactory> =
factories +
when (options[SchemaOption.FORMAT_BEHAVIOR_OPTION]) {
null, FormatBehavior.ANNOTATION_AND_ASSERTION -> FormatAssertionFactory.AnnotationAndAssertion
FormatBehavior.ANNOTATION_ONLY -> FormatAssertionFactory.AnnotationOnly
}

override val keywordResolver: KeyWordResolver
get() = Draft6KeyWordResolver
override val referenceFactory: ReferenceFactory
get() = Draft6ReferenceFactory
}

private object Draft6KeyWordResolver : KeyWordResolver {
private const val DEFINITIONS_PROPERTY: String = "definitions"
private const val ID_PROPERTY: String = "\$id"
const val REF_PROPERTY: String = "\$ref"

override fun resolve(keyword: KeyWord): String? =
when (keyword) {
ID -> ID_PROPERTY
DEFINITIONS -> DEFINITIONS_PROPERTY
ANCHOR, COMPATIBILITY_DEFINITIONS, DYNAMIC_ANCHOR -> null
}
}

private object Draft6ReferenceFactory : ReferenceFactory {
override fun extractRef(
schemaDefinition: JsonObject,
context: SchemaLoaderContext,
): RefHolder? =
if (REF_PROPERTY in schemaDefinition) {
RefHolder.Simple(REF_PROPERTY, schemaDefinition.getStringRequired(REF_PROPERTY).let(context::ref))
} else {
null
}

override val allowOverriding: Boolean
get() = false
override val resolveRefPriorId: Boolean
get() = false

override fun recursiveResolutionEnabled(schemaDefinition: JsonObject): Boolean = true
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package io.github.optimumcode.json.schema.internal.wellknown

internal enum class Draft6(
val content: String,
) {
SCHEMA(
"""
{
"${"$"}schema": "http://json-schema.org/draft-06/schema#",
"${"$"}id": "http://json-schema.org/draft-06/schema#",
"title": "Core schema meta-schema",
"definitions": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "${"$"}ref": "#" }
},
"nonNegativeInteger": {
"type": "integer",
"minimum": 0
},
"nonNegativeIntegerDefault0": {
"allOf": [
{ "${"$"}ref": "#/definitions/nonNegativeInteger" },
{ "default": 0 }
]
},
"simpleTypes": {
"enum": [
"array",
"boolean",
"integer",
"null",
"number",
"object",
"string"
]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"uniqueItems": true,
"default": []
}
},
"type": ["object", "boolean"],
"properties": {
"${"$"}id": {
"type": "string",
"format": "uri-reference"
},
"${"$"}schema": {
"type": "string",
"format": "uri"
},
"${"$"}ref": {
"type": "string",
"format": "uri-reference"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": {},
"examples": {
"type": "array",
"items": {}
},
"multipleOf": {
"type": "number",
"exclusiveMinimum": 0
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "number"
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "number"
},
"maxLength": { "${"$"}ref": "#/definitions/nonNegativeInteger" },
"minLength": { "${"$"}ref": "#/definitions/nonNegativeIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"additionalItems": { "${"$"}ref": "#" },
"items": {
"anyOf": [
{ "${"$"}ref": "#" },
{ "${"$"}ref": "#/definitions/schemaArray" }
],
"default": {}
},
"maxItems": { "${"$"}ref": "#/definitions/nonNegativeInteger" },
"minItems": { "${"$"}ref": "#/definitions/nonNegativeIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"contains": { "${"$"}ref": "#" },
"maxProperties": { "${"$"}ref": "#/definitions/nonNegativeInteger" },
"minProperties": { "${"$"}ref": "#/definitions/nonNegativeIntegerDefault0" },
"required": { "${"$"}ref": "#/definitions/stringArray" },
"additionalProperties": { "${"$"}ref": "#" },
"definitions": {
"type": "object",
"additionalProperties": { "${"$"}ref": "#" },
"default": {}
},
"properties": {
"type": "object",
"additionalProperties": { "${"$"}ref": "#" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "${"$"}ref": "#" },
"propertyNames": { "format": "regex" },
"default": {}
},
"dependencies": {
"type": "object",
"additionalProperties": {
"anyOf": [
{ "${"$"}ref": "#" },
{ "${"$"}ref": "#/definitions/stringArray" }
]
}
},
"propertyNames": { "${"$"}ref": "#" },
"const": {},
"enum": {
"type": "array",
"minItems": 1,
"uniqueItems": true
},
"type": {
"anyOf": [
{ "${"$"}ref": "#/definitions/simpleTypes" },
{
"type": "array",
"items": { "${"$"}ref": "#/definitions/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
},
"format": { "type": "string" },
"allOf": { "${"$"}ref": "#/definitions/schemaArray" },
"anyOf": { "${"$"}ref": "#/definitions/schemaArray" },
"oneOf": { "${"$"}ref": "#/definitions/schemaArray" },
"not": { "${"$"}ref": "#" }
},
"default": {}
}
""",
),
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,16 @@ class JsonSchemaExtensionTest : FunSpec() {
init {
test("reports keyword that matches one of the existing keywords") {
shouldThrow<IllegalStateException> {
JsonSchemaLoader.create()
JsonSchemaLoader
.create()
.withExtensions(DuplicatedAssertionFactory)
}.message shouldBe "external factory with keyword 'type' overlaps with 'type' keyword from DRAFT_7"
}.message shouldBe "external factory with keyword 'type' overlaps with 'type' keyword from DRAFT_6"
}

test("reports duplicated extension keywords") {
shouldThrow<IllegalStateException> {
JsonSchemaLoader.create()
JsonSchemaLoader
.create()
.withExtensions(SimpleDateFormatAssertionFactory, SimpleDateFormatAssertionFactory)
}.message shouldBe "duplicated extension factory with keyword 'dateFormat'"
}
Expand Down Expand Up @@ -93,7 +95,8 @@ class JsonSchemaExtensionTest : FunSpec() {
test("registers all extensions with varargs") {
val schema =
shouldNotThrowAny {
JsonSchemaLoader.create()
JsonSchemaLoader
.create()
.withExtensions(SimpleTimeFormatAssertionFactory, SimpleDateFormatAssertionFactory)
.fromDefinition(schemaDef)
}
Expand All @@ -103,7 +106,8 @@ class JsonSchemaExtensionTest : FunSpec() {
test("registers all extensions with iterable") {
val schema =
shouldNotThrowAny {
JsonSchemaLoader.create()
JsonSchemaLoader
.create()
.withExtensions(listOf(SimpleTimeFormatAssertionFactory, SimpleDateFormatAssertionFactory))
.fromDefinition(schemaDef)
}
Expand Down
Loading
Loading