Skip to content

Create JsonSchemaLoader interface to support external schema loading #42

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 23 commits into from
Jan 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
d54ce16
Create JsonSchemaLoader interface to support external schema loading
OptimumCode Jan 28, 2024
5e6fc08
Correct schema generation command to work on windows
OptimumCode Jan 28, 2024
99aa389
Try another python command
OptimumCode Jan 28, 2024
794ce47
Try closing output stream when exec is finished
OptimumCode Jan 29, 2024
bf6b907
Try customize command for windows
OptimumCode Jan 29, 2024
789f1e1
Try /c option for cmd
OptimumCode Jan 29, 2024
8ecc544
Try using bytearrayoutput and then copy data to file
OptimumCode Jan 29, 2024
2311877
Try without inputs/outputs declaration
OptimumCode Jan 29, 2024
4e1a4eb
Try not to use file method
OptimumCode Jan 29, 2024
e9d58d3
Try using executable and args separately
OptimumCode Jan 29, 2024
832eb25
Try using Path api for creating paths
OptimumCode Jan 29, 2024
c1fa9c6
Move remote schemas generation to another step in CI pipeline
OptimumCode Jan 29, 2024
0e3f118
Add missing checkout step
OptimumCode Jan 29, 2024
65ed899
Add missing java setup and correct env variable usage
OptimumCode Jan 29, 2024
6a417af
Correct artifact name
OptimumCode Jan 29, 2024
96db6e9
Add path for restoring artifacts
OptimumCode Jan 29, 2024
0fc0f81
Correct download artifact path
OptimumCode Jan 30, 2024
e1253b1
Properly close resources
OptimumCode Jan 30, 2024
f583950
Rollback changes to workflows
OptimumCode Jan 30, 2024
ff5398e
Add comments and minor code correction
OptimumCode Jan 30, 2024
6dd371e
Make assertion property in schema private
OptimumCode Jan 30, 2024
433aac2
Add ignore path to PR CI workflow
OptimumCode Jan 30, 2024
eed8aa1
Update readme
OptimumCode Jan 30, 2024
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 .ci-python-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3.10
3 changes: 3 additions & 0 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ jobs:
with:
distribution: temurin
java-version-file: .ci-java-version
- uses: actions/setup-python@v5
with:
python-version-file: .ci-python-version
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1.1.0
- name: Cache konan
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/pull_request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ name: "Check the PR"

on:
pull_request:
paths-ignore:
- 'README.md'
- 'changelog_config.json'

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ jobs:
with:
distribution: temurin
java-version-file: .ci-java-version
- uses: actions/setup-python@v5
with:
python-version-file: .ci-python-version
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1.1.0
- name: Cache konan
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/snapshot_release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ jobs:
with:
distribution: temurin
java-version-file: .ci-java-version
- uses: actions/setup-python@v5
with:
python-version-file: .ci-python-version
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1.1.0
- name: Cache konan
Expand Down
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ val valid = schema.validate(elementToValidate, errors::add)
|:------------|:----------------------------------------------------------------------------------------------------|
| $id | Supported. $id in sub-schemas are collected as well and can be used in $ref |
| $schema | Supported. Validates if schema is one of the supported schemas. The last supported is used if empty |
| $ref | Supported (except references to schemas from another document) |
| $ref | Supported |
| definitions | Supported. Definitions are loaded and can be referenced |

- Assertions
Expand Down Expand Up @@ -180,8 +180,8 @@ val valid = schema.validate(elementToValidate, errors::add)
|:------------------|:----------------------------------------------------------------------------------------------------|
| $id | Supported. $id in sub-schemas are collected as well and can be used in $ref |
| $schema | Supported. Validates if schema is one of the supported schemas. The last supported is used if empty |
| $ref | Supported (except references to schemas from another document) |
| $recursiveRef | Supported (does not work yet to extend schemas from other documents) |
| $ref | Supported |
| $recursiveRef | Supported |
| $defs/definitions | Supported. Definitions are loaded and can be referenced |

- Assertions
Expand Down Expand Up @@ -233,8 +233,8 @@ val valid = schema.validate(elementToValidate, errors::add)
|:---------------------------|:----------------------------------------------------------------------------------------------------|
| $id | Supported. $id in sub-schemas are collected as well and can be used in $ref |
| $schema | Supported. Validates if schema is one of the supported schemas. The last supported is used if empty |
| $ref | Supported (except references to schemas from another document) |
| $dynamicRef/$dynamicAnchor | Supported (does not work yet to extend schemas from other documents) |
| $ref | Supported |
| $dynamicRef/$dynamicAnchor | Supported |
| $defs/definitions | Supported. Definitions are loaded and can be referenced |

- Assertions
Expand Down Expand Up @@ -284,6 +284,9 @@ as a part of the CI to make sure the validation meet the expected behavior.
Not everything is supported right now but the missing functionality might be added in the future.
The test are located [here](test-suites).

**NOTE:** _Python 3.* is required to run test-suites._
_It is used to generate list of remote schemas using [this script](test-suites/schema-test-suite/bin/jsonschema_suite)_

## Developer notes

The update to Kotlin 1.9.22 came with an issue for JS incremental compilation.
Expand All @@ -296,7 +299,7 @@ In case you see an error about main function that already bind please execute `c
- [x] Add support for newer drafts
- [x] [Draft 2019-09 (Draft 8)](https://json-schema.org/specification-links.html#draft-2019-09-formerly-known-as-draft-8)
- [x] [2020-12](https://json-schema.org/specification-links.html#2020-12)
- [ ] Add support for schemas from external documents
- [ ] Load schemas from local sources
- [x] Add support for schemas from external documents
- [x] Load schemas from local sources
- [ ] Load schemas from remote sources
- [ ] Formalize error output as it is defined in the latest drafts (have not fully decided if it should be done)
29 changes: 29 additions & 0 deletions api/json-schema-validator.api
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,35 @@ public final class io/github/optimumcode/json/schema/JsonSchema$Companion {
public static synthetic fun fromJsonElement$default (Lio/github/optimumcode/json/schema/JsonSchema$Companion;Lkotlinx/serialization/json/JsonElement;Lio/github/optimumcode/json/schema/SchemaType;ILjava/lang/Object;)Lio/github/optimumcode/json/schema/JsonSchema;
}

public abstract interface class io/github/optimumcode/json/schema/JsonSchemaLoader {
public static final field Companion Lio/github/optimumcode/json/schema/JsonSchemaLoader$Companion;
public static fun create ()Lio/github/optimumcode/json/schema/JsonSchemaLoader;
public abstract fun fromDefinition (Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchema;
public abstract fun fromDefinition (Ljava/lang/String;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchema;
public abstract fun fromJsonElement (Lkotlinx/serialization/json/JsonElement;)Lio/github/optimumcode/json/schema/JsonSchema;
public abstract fun fromJsonElement (Lkotlinx/serialization/json/JsonElement;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchema;
public abstract fun register (Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
public abstract fun register (Ljava/lang/String;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
public abstract fun register (Lkotlinx/serialization/json/JsonElement;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
public abstract fun register (Lkotlinx/serialization/json/JsonElement;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
public abstract fun register (Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
public abstract fun register (Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
public abstract fun registerWellKnown (Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
}

public final class io/github/optimumcode/json/schema/JsonSchemaLoader$Companion {
public final fun create ()Lio/github/optimumcode/json/schema/JsonSchemaLoader;
}

public final class io/github/optimumcode/json/schema/JsonSchemaLoader$DefaultImpls {
public static fun fromDefinition (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchema;
public static fun fromJsonElement (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Lkotlinx/serialization/json/JsonElement;)Lio/github/optimumcode/json/schema/JsonSchema;
public static fun register (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
public static fun register (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Lkotlinx/serialization/json/JsonElement;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
public static fun register (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
public static fun registerWellKnown (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
}

public final class io/github/optimumcode/json/schema/JsonSchemaStream {
public static final fun fromStream (Lio/github/optimumcode/json/schema/JsonSchema$Companion;Ljava/io/InputStream;)Lio/github/optimumcode/json/schema/JsonSchema;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
package io.github.optimumcode.json.schema

import io.github.optimumcode.json.pointer.JsonPointer
import io.github.optimumcode.json.schema.internal.AssertionWithPath
import io.github.optimumcode.json.schema.internal.DefaultAssertionContext
import io.github.optimumcode.json.schema.internal.DefaultReferenceResolver
import io.github.optimumcode.json.schema.internal.IsolatedLoader
import io.github.optimumcode.json.schema.internal.JsonSchemaAssertion
import io.github.optimumcode.json.schema.internal.RefId
import io.github.optimumcode.json.schema.internal.SchemaLoader
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlin.jvm.JvmOverloads
import kotlin.jvm.JvmStatic
Expand All @@ -18,7 +15,7 @@ import kotlin.jvm.JvmStatic
*/
public class JsonSchema internal constructor(
private val assertion: JsonSchemaAssertion,
private val references: Map<RefId, AssertionWithPath>,
private val referenceResolver: DefaultReferenceResolver,
) {
/**
* Validates [value] against this [JsonSchema].
Expand All @@ -31,7 +28,7 @@ public class JsonSchema internal constructor(
value: JsonElement,
errorCollector: ErrorCollector,
): Boolean {
val context = DefaultAssertionContext(JsonPointer.ROOT, DefaultReferenceResolver(references))
val context = DefaultAssertionContext(JsonPointer.ROOT, referenceResolver)
return assertion.validate(value, context, errorCollector)
}

Expand All @@ -47,10 +44,7 @@ public class JsonSchema internal constructor(
public fun fromDefinition(
schema: String,
defaultType: SchemaType? = null,
): JsonSchema {
val schemaElement: JsonElement = Json.parseToJsonElement(schema)
return fromJsonElement(schemaElement, defaultType)
}
): JsonSchema = IsolatedLoader.fromDefinition(schema, defaultType)

/**
* Loads JSON schema from the [schemaElement] JSON element
Expand All @@ -63,8 +57,6 @@ public class JsonSchema internal constructor(
public fun fromJsonElement(
schemaElement: JsonElement,
defaultType: SchemaType? = null,
): JsonSchema {
return SchemaLoader().load(schemaElement, defaultType)
}
): JsonSchema = IsolatedLoader.fromJsonElement(schemaElement, defaultType)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package io.github.optimumcode.json.schema

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_7
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.Draft7
import kotlinx.serialization.json.JsonElement
import kotlin.jvm.JvmStatic

public interface JsonSchemaLoader {
public fun registerWellKnown(draft: SchemaType): JsonSchemaLoader =
apply {
when (draft) {
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) }
}
}

public fun register(schema: JsonElement): JsonSchemaLoader = register(schema, null)

public fun register(
schema: JsonElement,
draft: SchemaType?,
): JsonSchemaLoader

public fun register(schema: String): JsonSchemaLoader = register(schema, null)

public fun register(
schema: String,
draft: SchemaType?,
): JsonSchemaLoader

public fun register(
schema: JsonElement,
remoteUri: String,
): JsonSchemaLoader = register(schema, remoteUri, null)

public fun register(
schema: JsonElement,
remoteUri: String,
draft: SchemaType?,
): JsonSchemaLoader

public fun fromDefinition(schema: String): JsonSchema = fromDefinition(schema, null)

public fun fromDefinition(
schema: String,
draft: SchemaType?,
): JsonSchema

public fun fromJsonElement(schemaElement: JsonElement): JsonSchema = fromJsonElement(schemaElement, null)

public fun fromJsonElement(
schemaElement: JsonElement,
draft: SchemaType?,
): JsonSchema

public companion object {
@JvmStatic
public fun create(): JsonSchemaLoader = SchemaLoader()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public enum class SchemaType(
// so, it definitely is not a supported schema ID
return null
}
return values().find {
return entries.find {
it.schemaId.run {
host == uri.host &&
port == uri.port &&
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.optimumcode.json.schema.internal

import com.eygraber.uri.Uri
import io.github.optimumcode.json.pointer.JsonPointer
import io.github.optimumcode.json.pointer.div
import io.github.optimumcode.json.pointer.get
Expand Down Expand Up @@ -51,7 +52,10 @@ internal interface AssertionContext {
*/
fun getRecursiveRoot(): JsonSchemaAssertion?

fun pushSchemaPath(path: JsonPointer)
fun pushSchemaPath(
path: JsonPointer,
baseId: Uri,
)

fun popSchemaPath()
}
Expand Down Expand Up @@ -104,8 +108,11 @@ internal data class DefaultAssertionContext(
return recursiveRoot
}

override fun pushSchemaPath(path: JsonPointer) {
referenceResolver.pushSchemaPath(path)
override fun pushSchemaPath(
path: JsonPointer,
baseId: Uri,
) {
referenceResolver.pushSchemaPath(path, baseId)
}

override fun popSchemaPath() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package io.github.optimumcode.json.schema.internal

import com.eygraber.uri.Uri
import io.github.optimumcode.json.pointer.JsonPointer
import io.github.optimumcode.json.schema.ErrorCollector
import kotlinx.serialization.json.JsonElement

internal class JsonSchemaRoot(
private val baseId: Uri,
private val schemaPath: JsonPointer,
private val assertions: Collection<JsonSchemaAssertion>,
private val canBeReferencedRecursively: Boolean,
Expand All @@ -20,7 +22,7 @@ internal class JsonSchemaRoot(
context.resetRecursiveRoot()
}
var result = true
context.pushSchemaPath(schemaPath)
context.pushSchemaPath(schemaPath, baseId)
assertions.forEach {
val valid = it.validate(element, context, errorCollector)
result = result and valid
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.optimumcode.json.schema.internal

import com.eygraber.uri.Uri
import io.github.optimumcode.json.pointer.JsonPointer
import io.github.optimumcode.json.pointer.internal.dropLast
import io.github.optimumcode.json.pointer.internal.length
Expand All @@ -13,7 +14,7 @@ internal interface ReferenceResolver {

internal class DefaultReferenceResolver(
private val references: Map<RefId, AssertionWithPath>,
private val schemaPathsStack: ArrayDeque<JsonPointer> = ArrayDeque(),
private val schemaPathsStack: ArrayDeque<Pair<JsonPointer, Uri>> = ArrayDeque(),
) : ReferenceResolver {
override fun ref(refId: RefId): Pair<JsonPointer, JsonSchemaAssertion> {
val resolvedRef = requireNotNull(references[refId]) { "$refId is not found" }
Expand All @@ -35,14 +36,20 @@ internal class DefaultReferenceResolver(

val resolvedDynamicRef =
findMostOuterRef(possibleDynamicRefs)
// Try to select by base id starting from the most outer uri in path to the current location
?: schemaPathsStack.firstNotNullOfOrNull { (_, uri) ->
possibleDynamicRefs.firstOrNull { it.baseId == uri }
}
// If no outer anchor found use the original ref
?: possibleDynamicRefs.firstOrNull()
?: originalRef
return resolvedDynamicRef.schemaPath to resolvedDynamicRef.assertion
}

fun pushSchemaPath(path: JsonPointer) {
schemaPathsStack.addLast(path)
fun pushSchemaPath(
path: JsonPointer,
baseId: Uri,
) {
schemaPathsStack.addLast(path to baseId)
}

fun popSchemaPath() {
Expand All @@ -54,11 +61,11 @@ internal class DefaultReferenceResolver(
// Try to find the most outer anchor to use
// Check every schema in the current chain
// If not matches - take the most outer by location
for (schemaPath in schemaPathsStack) {
for ((schemaPath, baseId) in schemaPathsStack) {
var currPath: JsonPointer = schemaPath
while (currPath != JsonPointer.ROOT) {
for (dynamicRef in possibleRefs) {
if (dynamicRef.schemaPath.startsWith(currPath)) {
if (dynamicRef.schemaPath.startsWith(currPath) && dynamicRef.baseId == baseId) {
return dynamicRef
}
}
Expand Down
Loading