Skip to content

Commit

Permalink
[vividus-plugin-json] Add step to validate JSON against schema
Browse files Browse the repository at this point in the history
  • Loading branch information
avinBar committed Feb 1, 2023
1 parent 321c018 commit 0e9466d
Show file tree
Hide file tree
Showing 10 changed files with 428 additions and 5 deletions.
68 changes: 68 additions & 0 deletions docs/modules/plugins/pages/plugin-json.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Then number of JSON elements from `${json-context}` by JSON path `$.product` is
== Steps

:json-path: https://github.com/json-path/JsonPath#path-examples[JSON Path]
:json-schema: https://json-schema.org/understanding-json-schema/[JSON Schema]

The steps syntax uses two internal (VIVIDUS-only) terms:

Expand Down Expand Up @@ -500,3 +501,70 @@ Then number of JSON elements from `
]
` by JSON path `$..accountId` is equal to 2
----

=== Validate JSON against schema

Validates JSON against {json-schema}.

[source,gherkin]
----
Then JSON `$json` is valid against schema `$schema`
----

* `$json` - The JSON to validate.
* `$schema` - The {json-schema}.

[IMPORTANT]
====
The step validates JSON according to https://json-schema.org/specification-links.html[schema specification] version provided in the schema itself, e.g.:
[source,gherkin]
----
"$schema": "https://json-schema.org/draft/2020-12/schema"
----
If the version is not present in the schema then JSON is validated according to https://json-schema.org/specification-links.html#2020-12[2020-12] version.
====

.Validate product for sale information
[source,gherkin]
----
Then JSON `
{
"productId": 1,
"productName": "A desk lamp",
"price": 12.50,
"tags": [ "lamp", "desk" ]
}
` is valid against schema `
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://example.com/product.schema.json",
"title": "Product",
"description": "A product from catalog",
"type": "object",
"properties": {
"productId": {
"description": "The unique identifier for a product",
"type": "integer"
},
"productName": {
"type": "string"
},
"price": {
"type": "number",
"exclusiveMinimum": 0
},
"tags": {
"description": "Tags for the product",
"type": "array",
"prefixItems": {
"type": "string",
"enum": ["lamp", "desk"]
},
"minItems": 1,
"uniqueItems": true
}
},
"required": [ "productId", "productName", "price", "tags" ]
}
`
----
1 change: 1 addition & 0 deletions vividus-plugin-json/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dependencies {
implementation(group: 'io.qameta.allure', name: 'allure-jsonunit', version: versions.allure) {
exclude (group: 'io.qameta.allure')
}
implementation(group: 'com.networknt', name: 'json-schema-validator', version: '1.0.76');

testImplementation platform(group: 'org.junit', name: 'junit-bom', version: '5.9.2')
testImplementation(group: 'org.junit.jupiter', name: 'junit-jupiter')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/*
* Copyright 2019-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.vividus.json.steps;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

import com.fasterxml.jackson.databind.JsonNode;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaException;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SpecVersion;
import com.networknt.schema.SpecVersionDetector;
import com.networknt.schema.ValidationMessage;

import org.jbehave.core.annotations.Then;
import org.vividus.softassert.ISoftAssert;
import org.vividus.util.json.JsonUtils;

public class JsonValidationSteps
{
private static final String SCHEMA_TAG = "$schema";

private final JsonUtils jsonUtils;
private final ISoftAssert softAssert;

public JsonValidationSteps(JsonUtils jsonUtils, ISoftAssert softAssert)
{
this.jsonUtils = jsonUtils;
this.softAssert = softAssert;
}

/**
* Validates json against json schema. The step validates JSON according to
* <a href="https://json-schema.org/specification-links.html">schema specification</a> version provided in the
* schema itself, e.g.: <br><i>"$schema": "https://json-schema.org/draft/2020-12/schema"</i><br>
* If the version is not present in the schema then JSON is validated according to
* <a href="https://json-schema.org/specification-links.html#2020-12">2020-12</a> version.
* @param json The JSON to validate.
* @param schema The JSON schema.
*/
@Then("JSON `$json` is valid against schema `$schema`")
public void validateJsonAgainstSchema(String json, String schema)
{
JsonNode schemaNode = jsonUtils.readTree(schema);
SpecVersion.VersionFlag version;
if (schemaNode.has(SCHEMA_TAG))
{
try
{
version = SpecVersionDetector.detect(schemaNode);
}
catch (JsonSchemaException e)
{
softAssert.recordFailedAssertion(
String.format("`%s` is unrecognizable schema", schemaNode.get(SCHEMA_TAG).asText()));
return;
}
}
else
{
version = SpecVersion.VersionFlag.V202012;
}
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(version);
JsonSchema jsonSchema = factory.getSchema(schemaNode);
JsonNode jsonNode = jsonUtils.readTree(json);
assertJsonValidationAgainstSchema(jsonSchema.validate(jsonNode));
}

private void assertJsonValidationAgainstSchema(Set<ValidationMessage> validationMessages)
{
if (validationMessages.isEmpty())
{
softAssert.recordPassedAssertion("JSON is valid against schema");
}
else
{
softAssert.recordFailedAssertion(String.format("JSON is not valid against schema:%n%s",
formatValidationMessages(validationMessages)));
}
}

private String formatValidationMessages(Set<ValidationMessage> validationMessages)
{
List<String> messages = new ArrayList<>();
int index = 1;
for (ValidationMessage validationMessage : validationMessages)
{
messages.add(String.format("%d) %s", index, validationMessage.toString()));
index++;
}
return String.join(String.format(",%n"), messages);
}
}
11 changes: 6 additions & 5 deletions vividus-plugin-json/src/main/resources/vividus-plugin/spring.xml
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,17 @@

<bean id="json-context" class="org.vividus.json.variable.JsonContextVariable" />

<bean id="jsonSteps" class="org.vividus.json.steps.JsonSteps">
<constructor-arg>
<bean class="org.vividus.json.softassert.JsonSoftAssert" parent="softAssert" />
</constructor-arg>
</bean>
<bean class="org.vividus.json.softassert.JsonSoftAssert" parent="softAssert" />

<bean id="jsonSteps" class="org.vividus.json.steps.JsonSteps"/>

<bean id="jsonPatchSteps" class="org.vividus.json.steps.JsonPatchSteps" />

<bean id="jsonValidationSteps" class="org.vividus.json.steps.JsonValidationSteps"/>

<util:list id="stepBeanNames-Json">
<idref bean="jsonSteps" />
<idref bean="jsonPatchSteps" />
<idref bean="jsonValidationSteps" />
</util:list>
</beans>
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright 2019-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.vividus.json.steps;

import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.vividus.softassert.ISoftAssert;
import org.vividus.util.ResourceUtils;
import org.vividus.util.json.JsonUtils;

@ExtendWith(MockitoExtension.class)
class JsonValidationStepsTests
{
@Mock
private ISoftAssert softAssert;

private JsonValidationSteps steps;

@BeforeEach
void beforeEach()
{
steps = new JsonValidationSteps(new JsonUtils(), softAssert);
}

@Test
void shouldValidateJsonAgainstSchema()
{
verifyValidationOfValidJson(loadSchema());
}

@Test
void shouldValidateJsonAgainstSchemaWithoutSchemaTag()
{
var schema = ResourceUtils.loadResource(getClass(), "schema-without-schema-tag.json");
verifyValidationOfValidJson(schema);
}

@Test
void shouldFailJSONSchemaWithWrongSpecification()
{
var schema = "{\"$schema\": \"wrong-specification\"}";
steps.validateJsonAgainstSchema(loadValidAgainstSchemaJson(), schema);
verify(softAssert).recordFailedAssertion("`wrong-specification` is unrecognizable schema");
verifyNoMoreInteractions(softAssert);
}

@Test
void shouldFailValidationAgainstSchema()
{
var notValidJson = ResourceUtils.loadResource(getClass(), "not-valid-against-schema.json");
steps.validateJsonAgainstSchema(notValidJson, loadSchema());
verify(softAssert).recordFailedAssertion(String.format("JSON is not valid against schema:%n1) $.price: is "
+ "missing but it is required,%n2) $.tags: integer found, array expected"));
verifyNoMoreInteractions(softAssert);
}

private void verifyValidationOfValidJson(String jsonSchema)
{
steps.validateJsonAgainstSchema(loadValidAgainstSchemaJson(), jsonSchema);
verify(softAssert).recordPassedAssertion("JSON is valid against schema");
verifyNoMoreInteractions(softAssert);
}

private String loadValidAgainstSchemaJson()
{
return ResourceUtils.loadResource(getClass(), "valid-against-schema.json");
}

private String loadSchema()
{
return ResourceUtils.loadResource(getClass(), "schema.json");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"productId": 1,
"productName": "A green door",
"tags": 1
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"$id": "https://example.com/product.schema.json",
"title": "Product",
"description": "A product from Acme's catalog",
"type": "object",
"properties": {
"productId": {
"description": "The unique identifier for a product",
"type": "integer"
},
"productName": {
"description": "Name of the product",
"type": "string"
},
"price": {
"description": "The price of the product",
"type": "number",
"exclusiveMinimum": 0
},
"tags": {
"description": "Tags for the product",
"type": "array",
"prefixItems": {
"type": "string"
},
"minItems": 1,
"uniqueItems": true
},
"dimensions": {
"type": "object",
"properties": {
"length": {
"type": "number"
},
"width": {
"type": "number"
},
"height": {
"type": "number"
}
},
"required": [ "length", "width", "height" ]
}
},
"required": [ "productId", "productName", "price" ]
}
Loading

0 comments on commit 0e9466d

Please sign in to comment.