Skip to content

Commit 0801b98

Browse files
authored
Create JsonSchemaLoader interface to support external schema loading (#42)
Resolves #27
1 parent 45aaa7a commit 0801b98

File tree

24 files changed

+1466
-142
lines changed

24 files changed

+1466
-142
lines changed

.ci-python-version

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
3.10

.github/workflows/check.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ jobs:
2424
with:
2525
distribution: temurin
2626
java-version-file: .ci-java-version
27+
- uses: actions/setup-python@v5
28+
with:
29+
python-version-file: .ci-python-version
2730
- name: Validate Gradle Wrapper
2831
uses: gradle/[email protected]
2932
- name: Cache konan

.github/workflows/pull_request.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ name: "Check the PR"
22

33
on:
44
pull_request:
5+
paths-ignore:
6+
- 'README.md'
7+
- 'changelog_config.json'
58

69
concurrency:
710
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}

.github/workflows/release.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ jobs:
3737
with:
3838
distribution: temurin
3939
java-version-file: .ci-java-version
40+
- uses: actions/setup-python@v5
41+
with:
42+
python-version-file: .ci-python-version
4043
- name: Validate Gradle Wrapper
4144
uses: gradle/[email protected]
4245
- name: Cache konan

.github/workflows/snapshot_release.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ jobs:
1717
with:
1818
distribution: temurin
1919
java-version-file: .ci-java-version
20+
- uses: actions/setup-python@v5
21+
with:
22+
python-version-file: .ci-python-version
2023
- name: Validate Gradle Wrapper
2124
uses: gradle/[email protected]
2225
- name: Cache konan

README.md

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ val valid = schema.validate(elementToValidate, errors::add)
133133
|:------------|:----------------------------------------------------------------------------------------------------|
134134
| $id | Supported. $id in sub-schemas are collected as well and can be used in $ref |
135135
| $schema | Supported. Validates if schema is one of the supported schemas. The last supported is used if empty |
136-
| $ref | Supported (except references to schemas from another document) |
136+
| $ref | Supported |
137137
| definitions | Supported. Definitions are loaded and can be referenced |
138138

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

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

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

287+
**NOTE:** _Python 3.* is required to run test-suites._
288+
_It is used to generate list of remote schemas using [this script](test-suites/schema-test-suite/bin/jsonschema_suite)_
289+
287290
## Developer notes
288291

289292
The update to Kotlin 1.9.22 came with an issue for JS incremental compilation.
@@ -296,7 +299,7 @@ In case you see an error about main function that already bind please execute `c
296299
- [x] Add support for newer drafts
297300
- [x] [Draft 2019-09 (Draft 8)](https://json-schema.org/specification-links.html#draft-2019-09-formerly-known-as-draft-8)
298301
- [x] [2020-12](https://json-schema.org/specification-links.html#2020-12)
299-
- [ ] Add support for schemas from external documents
300-
- [ ] Load schemas from local sources
302+
- [x] Add support for schemas from external documents
303+
- [x] Load schemas from local sources
301304
- [ ] Load schemas from remote sources
302305
- [ ] Formalize error output as it is defined in the latest drafts (have not fully decided if it should be done)

api/json-schema-validator.api

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,35 @@ public final class io/github/optimumcode/json/schema/JsonSchema$Companion {
5656
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;
5757
}
5858

59+
public abstract interface class io/github/optimumcode/json/schema/JsonSchemaLoader {
60+
public static final field Companion Lio/github/optimumcode/json/schema/JsonSchemaLoader$Companion;
61+
public static fun create ()Lio/github/optimumcode/json/schema/JsonSchemaLoader;
62+
public abstract fun fromDefinition (Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchema;
63+
public abstract fun fromDefinition (Ljava/lang/String;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchema;
64+
public abstract fun fromJsonElement (Lkotlinx/serialization/json/JsonElement;)Lio/github/optimumcode/json/schema/JsonSchema;
65+
public abstract fun fromJsonElement (Lkotlinx/serialization/json/JsonElement;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchema;
66+
public abstract fun register (Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
67+
public abstract fun register (Ljava/lang/String;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
68+
public abstract fun register (Lkotlinx/serialization/json/JsonElement;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
69+
public abstract fun register (Lkotlinx/serialization/json/JsonElement;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
70+
public abstract fun register (Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
71+
public abstract fun register (Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
72+
public abstract fun registerWellKnown (Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
73+
}
74+
75+
public final class io/github/optimumcode/json/schema/JsonSchemaLoader$Companion {
76+
public final fun create ()Lio/github/optimumcode/json/schema/JsonSchemaLoader;
77+
}
78+
79+
public final class io/github/optimumcode/json/schema/JsonSchemaLoader$DefaultImpls {
80+
public static fun fromDefinition (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchema;
81+
public static fun fromJsonElement (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Lkotlinx/serialization/json/JsonElement;)Lio/github/optimumcode/json/schema/JsonSchema;
82+
public static fun register (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
83+
public static fun register (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Lkotlinx/serialization/json/JsonElement;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
84+
public static fun register (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Lkotlinx/serialization/json/JsonElement;Ljava/lang/String;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
85+
public static fun registerWellKnown (Lio/github/optimumcode/json/schema/JsonSchemaLoader;Lio/github/optimumcode/json/schema/SchemaType;)Lio/github/optimumcode/json/schema/JsonSchemaLoader;
86+
}
87+
5988
public final class io/github/optimumcode/json/schema/JsonSchemaStream {
6089
public static final fun fromStream (Lio/github/optimumcode/json/schema/JsonSchema$Companion;Ljava/io/InputStream;)Lio/github/optimumcode/json/schema/JsonSchema;
6190
}

src/commonMain/kotlin/io/github/optimumcode/json/schema/JsonSchema.kt

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,10 @@
11
package io.github.optimumcode.json.schema
22

33
import io.github.optimumcode.json.pointer.JsonPointer
4-
import io.github.optimumcode.json.schema.internal.AssertionWithPath
54
import io.github.optimumcode.json.schema.internal.DefaultAssertionContext
65
import io.github.optimumcode.json.schema.internal.DefaultReferenceResolver
6+
import io.github.optimumcode.json.schema.internal.IsolatedLoader
77
import io.github.optimumcode.json.schema.internal.JsonSchemaAssertion
8-
import io.github.optimumcode.json.schema.internal.RefId
9-
import io.github.optimumcode.json.schema.internal.SchemaLoader
10-
import kotlinx.serialization.json.Json
118
import kotlinx.serialization.json.JsonElement
129
import kotlin.jvm.JvmOverloads
1310
import kotlin.jvm.JvmStatic
@@ -18,7 +15,7 @@ import kotlin.jvm.JvmStatic
1815
*/
1916
public class JsonSchema internal constructor(
2017
private val assertion: JsonSchemaAssertion,
21-
private val references: Map<RefId, AssertionWithPath>,
18+
private val referenceResolver: DefaultReferenceResolver,
2219
) {
2320
/**
2421
* Validates [value] against this [JsonSchema].
@@ -31,7 +28,7 @@ public class JsonSchema internal constructor(
3128
value: JsonElement,
3229
errorCollector: ErrorCollector,
3330
): Boolean {
34-
val context = DefaultAssertionContext(JsonPointer.ROOT, DefaultReferenceResolver(references))
31+
val context = DefaultAssertionContext(JsonPointer.ROOT, referenceResolver)
3532
return assertion.validate(value, context, errorCollector)
3633
}
3734

@@ -47,10 +44,7 @@ public class JsonSchema internal constructor(
4744
public fun fromDefinition(
4845
schema: String,
4946
defaultType: SchemaType? = null,
50-
): JsonSchema {
51-
val schemaElement: JsonElement = Json.parseToJsonElement(schema)
52-
return fromJsonElement(schemaElement, defaultType)
53-
}
47+
): JsonSchema = IsolatedLoader.fromDefinition(schema, defaultType)
5448

5549
/**
5650
* Loads JSON schema from the [schemaElement] JSON element
@@ -63,8 +57,6 @@ public class JsonSchema internal constructor(
6357
public fun fromJsonElement(
6458
schemaElement: JsonElement,
6559
defaultType: SchemaType? = null,
66-
): JsonSchema {
67-
return SchemaLoader().load(schemaElement, defaultType)
68-
}
60+
): JsonSchema = IsolatedLoader.fromJsonElement(schemaElement, defaultType)
6961
}
7062
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package io.github.optimumcode.json.schema
2+
3+
import io.github.optimumcode.json.schema.SchemaType.DRAFT_2019_09
4+
import io.github.optimumcode.json.schema.SchemaType.DRAFT_2020_12
5+
import io.github.optimumcode.json.schema.SchemaType.DRAFT_7
6+
import io.github.optimumcode.json.schema.internal.SchemaLoader
7+
import io.github.optimumcode.json.schema.internal.wellknown.Draft201909
8+
import io.github.optimumcode.json.schema.internal.wellknown.Draft202012
9+
import io.github.optimumcode.json.schema.internal.wellknown.Draft7
10+
import kotlinx.serialization.json.JsonElement
11+
import kotlin.jvm.JvmStatic
12+
13+
public interface JsonSchemaLoader {
14+
public fun registerWellKnown(draft: SchemaType): JsonSchemaLoader =
15+
apply {
16+
when (draft) {
17+
DRAFT_7 -> Draft7.entries.forEach { register(it.content) }
18+
DRAFT_2019_09 -> Draft201909.entries.forEach { register(it.content) }
19+
DRAFT_2020_12 -> Draft202012.entries.forEach { register(it.content) }
20+
}
21+
}
22+
23+
public fun register(schema: JsonElement): JsonSchemaLoader = register(schema, null)
24+
25+
public fun register(
26+
schema: JsonElement,
27+
draft: SchemaType?,
28+
): JsonSchemaLoader
29+
30+
public fun register(schema: String): JsonSchemaLoader = register(schema, null)
31+
32+
public fun register(
33+
schema: String,
34+
draft: SchemaType?,
35+
): JsonSchemaLoader
36+
37+
public fun register(
38+
schema: JsonElement,
39+
remoteUri: String,
40+
): JsonSchemaLoader = register(schema, remoteUri, null)
41+
42+
public fun register(
43+
schema: JsonElement,
44+
remoteUri: String,
45+
draft: SchemaType?,
46+
): JsonSchemaLoader
47+
48+
public fun fromDefinition(schema: String): JsonSchema = fromDefinition(schema, null)
49+
50+
public fun fromDefinition(
51+
schema: String,
52+
draft: SchemaType?,
53+
): JsonSchema
54+
55+
public fun fromJsonElement(schemaElement: JsonElement): JsonSchema = fromJsonElement(schemaElement, null)
56+
57+
public fun fromJsonElement(
58+
schemaElement: JsonElement,
59+
draft: SchemaType?,
60+
): JsonSchema
61+
62+
public companion object {
63+
@JvmStatic
64+
public fun create(): JsonSchemaLoader = SchemaLoader()
65+
}
66+
}

src/commonMain/kotlin/io/github/optimumcode/json/schema/SchemaType.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public enum class SchemaType(
2828
// so, it definitely is not a supported schema ID
2929
return null
3030
}
31-
return values().find {
31+
return entries.find {
3232
it.schemaId.run {
3333
host == uri.host &&
3434
port == uri.port &&

src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/AssertionContext.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.github.optimumcode.json.schema.internal
22

3+
import com.eygraber.uri.Uri
34
import io.github.optimumcode.json.pointer.JsonPointer
45
import io.github.optimumcode.json.pointer.div
56
import io.github.optimumcode.json.pointer.get
@@ -51,7 +52,10 @@ internal interface AssertionContext {
5152
*/
5253
fun getRecursiveRoot(): JsonSchemaAssertion?
5354

54-
fun pushSchemaPath(path: JsonPointer)
55+
fun pushSchemaPath(
56+
path: JsonPointer,
57+
baseId: Uri,
58+
)
5559

5660
fun popSchemaPath()
5761
}
@@ -104,8 +108,11 @@ internal data class DefaultAssertionContext(
104108
return recursiveRoot
105109
}
106110

107-
override fun pushSchemaPath(path: JsonPointer) {
108-
referenceResolver.pushSchemaPath(path)
111+
override fun pushSchemaPath(
112+
path: JsonPointer,
113+
baseId: Uri,
114+
) {
115+
referenceResolver.pushSchemaPath(path, baseId)
109116
}
110117

111118
override fun popSchemaPath() {

src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/JsonSchemaRoot.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
package io.github.optimumcode.json.schema.internal
22

3+
import com.eygraber.uri.Uri
34
import io.github.optimumcode.json.pointer.JsonPointer
45
import io.github.optimumcode.json.schema.ErrorCollector
56
import kotlinx.serialization.json.JsonElement
67

78
internal class JsonSchemaRoot(
9+
private val baseId: Uri,
810
private val schemaPath: JsonPointer,
911
private val assertions: Collection<JsonSchemaAssertion>,
1012
private val canBeReferencedRecursively: Boolean,
@@ -20,7 +22,7 @@ internal class JsonSchemaRoot(
2022
context.resetRecursiveRoot()
2123
}
2224
var result = true
23-
context.pushSchemaPath(schemaPath)
25+
context.pushSchemaPath(schemaPath, baseId)
2426
assertions.forEach {
2527
val valid = it.validate(element, context, errorCollector)
2628
result = result and valid

src/commonMain/kotlin/io/github/optimumcode/json/schema/internal/ReferenceResolver.kt

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package io.github.optimumcode.json.schema.internal
22

3+
import com.eygraber.uri.Uri
34
import io.github.optimumcode.json.pointer.JsonPointer
45
import io.github.optimumcode.json.pointer.internal.dropLast
56
import io.github.optimumcode.json.pointer.internal.length
@@ -13,7 +14,7 @@ internal interface ReferenceResolver {
1314

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

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

44-
fun pushSchemaPath(path: JsonPointer) {
45-
schemaPathsStack.addLast(path)
48+
fun pushSchemaPath(
49+
path: JsonPointer,
50+
baseId: Uri,
51+
) {
52+
schemaPathsStack.addLast(path to baseId)
4653
}
4754

4855
fun popSchemaPath() {
@@ -54,11 +61,11 @@ internal class DefaultReferenceResolver(
5461
// Try to find the most outer anchor to use
5562
// Check every schema in the current chain
5663
// If not matches - take the most outer by location
57-
for (schemaPath in schemaPathsStack) {
64+
for ((schemaPath, baseId) in schemaPathsStack) {
5865
var currPath: JsonPointer = schemaPath
5966
while (currPath != JsonPointer.ROOT) {
6067
for (dynamicRef in possibleRefs) {
61-
if (dynamicRef.schemaPath.startsWith(currPath)) {
68+
if (dynamicRef.schemaPath.startsWith(currPath) && dynamicRef.baseId == baseId) {
6269
return dynamicRef
6370
}
6471
}

0 commit comments

Comments
 (0)