Skip to content

Commit db19e49

Browse files
authored
Add documentation and example for creating custom assertions (#62)
1 parent f16f5c9 commit db19e49

File tree

2 files changed

+165
-0
lines changed

2 files changed

+165
-0
lines changed

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,10 @@ val valid = schema.validate(elementToValidate, errors::add)
278278
| | not | Supported |
279279
</details>
280280

281+
## Custom assertions
282+
283+
You can implement custom assertions and use them. Read more [here](docs/custom_assertions.md).
284+
281285
## Compliance to JSON schema test suites
282286

283287
This library uses official [JSON schema test suites](https://github.com/json-schema-org/JSON-Schema-Test-Suite)

docs/custom_assertions.md

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# Create and use custom assertions
2+
3+
The library provides functionality to create custom assertions that can be used in JSON schemes then.
4+
Here you can see how you can add, register and use the custom assertion.
5+
6+
## Creating custom assertion
7+
8+
There are two interfaces you need to use here:
9+
1. [ExternalAssertionFactory](../src/commonMain/kotlin/io/github/optimumcode/json/schema/extension/ExternalAssertionFactory.kt) -
10+
this interface creates an assertions from a JsonElement.
11+
2. [ExternalAssertion](../src/commonMain/kotlin/io/github/optimumcode/json/schema/extension/ExternalAssertion.kt) -
12+
this interface is the actual implementation of the assertion that validates the corresponding element.
13+
14+
### ExternalAssertionFactory interface
15+
16+
The `ExternalAssertionFactory` interface is quite simple.
17+
You need to implement `keywordName` property that returns a keyword associated with the assertion.
18+
19+
Another method is `create` that instantiates an `ExternalAssertion` implementation based on the `JsonElement` passed as a parameter.
20+
There is also another parameter [ExternalLoadingContext](../src/commonMain/kotlin/io/github/optimumcode/json/schema/extension/ExternalLoadingContext.kt).
21+
This object provides you information about current location in schema which should be used later
22+
when create a [ValidationError](../src/commonMain/kotlin/io/github/optimumcode/json/schema/ValidationError.kt).
23+
24+
Almost always the `ExternalAssertionFactory` does not have any state.
25+
Because of that it is better to create it as a Kotlin `object` instead of regular `class`.
26+
27+
### ExternalAssertion interface
28+
29+
The `ExternalAssertion` interface has only one method `validate`.
30+
This method performs required actions and decides whether the passed `JsonElement` passes the assertion.
31+
There are two more parameters in this method:
32+
* [ExternalAssertionContext](../src/commonMain/kotlin/io/github/optimumcode/json/schema/extension/ExternalAssertionContext.kt);
33+
* [ErrorCollector](../src/commonMain/kotlin/io/github/optimumcode/json/schema/ErrorCollector.kt).
34+
35+
`ExternalAssertionContext` contains information associated with currently validating `JsonElement`.
36+
It has a JSON path that point to the location of the current `JsonElement`
37+
and [ExternalAnnotationCollector](../src/commonMain/kotlin/io/github/optimumcode/json/schema/extension/ExternalAnnotationCollector.kt).
38+
The later one provides the way to add annotations to the `JsonElement` and communicate between different assertion using those.
39+
40+
## Registering custom assertions
41+
42+
You can register custom assertions using `withExtensions` methods on `JsonSchemaLoader` instance.
43+
44+
**NOTE: the order of registration is important in case the assertions uses annotations.**
45+
**The assertions will be executed in the same order as their factories were registered.**
46+
47+
## Example
48+
49+
Let's try to implement a small assertion `dateFormat` that accepts a value `iso` and checks that the `JsonElement` matches ISO date format.
50+
51+
**NOTE: the example implementation does not check the date part values**
52+
53+
First we need to create an `ExternalAssertionFactory`:
54+
55+
```kotlin
56+
import io.github.optimumcode.json.schema.extension.*
57+
import kotlinx.serialization.json.*
58+
59+
object DateFormatAssertionFactory : ExternalAssertionFactory {
60+
private const val PROPERTY: String = "dateFormat"
61+
override val keywordName: String
62+
get() = PROPERTY // 1 - the keyword that is used for our assertion
63+
64+
override fun create(
65+
element: JsonElement,
66+
context: ExternalLoadingContext,
67+
): ExternalAssertion {
68+
require(element is JsonPrimitive && element.isString) { // 2 - validate the element
69+
"$PROPERTY must be a string"
70+
}
71+
val formatType: String = element.content
72+
require(formatType.equals("iso", ignoreCase = true)) { // 3 - we only support one format for now
73+
"$PROPERTY has unsupported value '$formatType'"
74+
}
75+
return DateFormatAssertion(
76+
context.schemaPath, // 4 - we pass the schema path to the assertion to use it later in case of validation error
77+
)
78+
}
79+
}
80+
```
81+
82+
Now we can create the `ExternalAssertion` itself:
83+
84+
```kotlin
85+
import io.github.optimumcode.json.schema.extension.*
86+
import io.github.optimumcode.json.schema.*
87+
import io.github.optimumcode.json.pointer.*
88+
import kotlinx.serialization.json.*
89+
90+
class DateFormatAssertion(
91+
private val schemaPath: JsonPointer,
92+
) : ExternalAssertion {
93+
override fun validate(
94+
element: JsonElement,
95+
context: ExternalAssertionContext,
96+
errorCollector: ErrorCollector,
97+
): Boolean {
98+
if (element !is JsonPrimitive || !element.isString) {
99+
return true // 1 - the assertion must ignore types that it does not expect. In our case the element must be a string
100+
}
101+
val matches = FORMAT_REGEX.matches(element.content) // 2 - checking the format
102+
if (!matches) {
103+
errorCollector.onError( // 3 - creating error if value does not match the expected format
104+
ValidationError(
105+
schemaPath = schemaPath, // 4 - set path to our keyword in schema
106+
objectPath = context.objectPath, // 5 - set path to the element in the object we validate
107+
message = "invalid date format", // 6 - specify the error message
108+
),
109+
)
110+
}
111+
return matches // 7 - return the validation result
112+
}
113+
114+
private companion object {
115+
private val FORMAT_REGEX = Regex("\\d{4}-\\d{2}-\\d{2}")
116+
}
117+
}
118+
```
119+
120+
Good. Once the assertion factory and assertion itself are implemented we can now register them and use.
121+
Here is a code snippet that creates JSON schema using our custom assertion:
122+
123+
```kotlin
124+
import io.github.optimumcode.json.schema.*
125+
126+
fun main() {
127+
val schema = JsonSchemaLoader.create()
128+
.withExtensions(DateFormatAssertionFactory)
129+
.fromDefinition(
130+
"""
131+
{
132+
"properties": {
133+
"date": {
134+
"type": "string",
135+
"dateFormat": "iso"
136+
}
137+
}
138+
}
139+
""".trimMargin()
140+
)
141+
142+
val validElement = toJsonElement(
143+
"""
144+
{
145+
"date": "2024-02-10"
146+
}
147+
""".trimMargin()
148+
)
149+
150+
val invalidElement = toJsonElement(
151+
"""
152+
{
153+
"date": "2024/02/10"
154+
}
155+
""".trimMargin()
156+
)
157+
158+
schema.validate(validElement, ErrorCollector.EMPTY) // returns true
159+
schema.validate(invalidElement, ErrorCollector.EMPTY) // returns false
160+
}
161+
```

0 commit comments

Comments
 (0)