Skip to content

Commit

Permalink
Basic implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
JaredHatfield committed Mar 13, 2024
1 parent ca433ff commit b6bda8c
Show file tree
Hide file tree
Showing 9 changed files with 382 additions and 2 deletions.
24 changes: 24 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,34 @@
</properties>

<dependencies>
<dependency>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>
<version>1.3.3</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>6.1.4</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2024 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 com.unitvectory.jsonschema4springboot;

import com.networknt.schema.JsonSchema;

/**
* JSON Schema Cache interface.
*
* @author Jared Hatfield (UnitVectorY Labs)
*/
public interface JsonSchemaCache {

/**
* Get a cached schema.
*
* @param path the path
* @return the JsonSchema; null if not found
*/
JsonSchema getSchema(String path);

/**
* Cache a JsonSchema
*
* @param path the path
* @param schema the JsonSchema
*/
void cacheSchema(String path, JsonSchema schema);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright 2024 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 com.unitvectory.jsonschema4springboot;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import com.networknt.schema.JsonSchema;

/**
* The Default JSON Schema Cache implementation.
*
* Caches an unbounded number of schemas perpetually.
*
* @author Jared Hatfield (UnitVectorY Labs)
*/
class JsonSchemaCacheDefault implements JsonSchemaCache {

private Map<String, JsonSchema> cache = new ConcurrentHashMap<>();

@Override
public JsonSchema getSchema(String path) {
return this.cache.get(path);
}

@Override
public void cacheSchema(String path, JsonSchema schema) {
this.cache.put(path, schema);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2024 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 com.unitvectory.jsonschema4springboot;

import com.networknt.schema.JsonSchema;
import com.networknt.schema.SpecVersion.VersionFlag;

/**
* JSON Schema lookup interface.
*
* @author Jared Hatfield (UnitVectorY Labs)
*/
public interface JsonSchemaLookup {

/**
* Get a cached schema.
*
* @param version the JSON Schema version
* @param path the path
* @return the JsonSchema; null if not found
* @throws ValidateJsonSchemaException schema was unable to be loaded
*/
JsonSchema getSchema(VersionFlag version, String path) throws ValidateJsonSchemaException;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2024 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 com.unitvectory.jsonschema4springboot;

import java.io.InputStream;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SpecVersion.VersionFlag;

/**
* JSON Schema lookup interface.
*
* This loads schemas from the class path by default.
*
* @author Jared Hatfield (UnitVectorY Labs)
*/
public class JsonSchemaLookupDefault implements JsonSchemaLookup {

@Override
public JsonSchema getSchema(VersionFlag version, String path)
throws ValidateJsonSchemaException {
// Get the factory for the specified version
JsonSchemaFactory schemaFactory = JsonSchemaFactory.getInstance(version);

// Load the resource from the class path
Resource resource = new ClassPathResource(path);
if (!resource.exists()) {
throw new ValidateJsonSchemaException("JSON Schema not found at path: " + path);
}

// Load the schema
try (InputStream schemaStream = resource.getInputStream()) {
return schemaFactory.getSchema(schemaStream);
} catch (Exception e) {
throw new ValidateJsonSchemaException("JSON Schema failed to load from path: " + path,
e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,24 @@
*/
package com.unitvectory.jsonschema4springboot;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Check the JSON Schema
* Validate the JSON Schema
*
* @author Jared Hatfield (UnitVectorY Labs)
*/
public @interface CheckJsonSchema {
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidateJsonSchema {

/**
* The path to the schema
*
* @return the path
*/
String schemaPath();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Copyright 2024 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 com.unitvectory.jsonschema4springboot;

import java.nio.charset.StandardCharsets;
import java.util.Set;
import javax.servlet.http.HttpServletRequest;
import org.springframework.core.MethodParameter;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;
import com.fasterxml.jackson.databind.JsonNode;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.ValidationMessage;
import lombok.AllArgsConstructor;

/**
* The argument resolver that supports JSON Schema validation.
*
* @author Jared Hatfield (UnitVectorY Labs)
*/
@AllArgsConstructor
public class ValidateJsonSchemaArgumentResolver implements HandlerMethodArgumentResolver {

/**
* The configuration
*/
private final ValidateJsonSchemaConfig config;

@Override
public final boolean supportsParameter(MethodParameter parameter) {
// Only applies to ValidateJsonSchema annotation
return parameter.hasParameterAnnotation(ValidateJsonSchema.class);
}

@Override
public final Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer, NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception {
// Get the annotation
String schemaPath = parameter.getParameterAnnotation(ValidateJsonSchema.class).schemaPath();

// First try the cache
JsonSchema schema = this.config.getCache().getSchema(schemaPath);
if (schema == null) {
schema = this.config.getLookup().getSchema(this.config.getSpecVersion(), schemaPath);

// Failed to get the
if (schema == null) {
throw new ValidateJsonSchemaException("Failed to load JSON Schema");
}

// Cache the value
this.config.getCache().cacheSchema(schemaPath, schema);
}

// Get the JSON as a String
HttpServletRequest httpServletRequest =
webRequest.getNativeRequest(HttpServletRequest.class);
String jsonString = StreamUtils.copyToString(httpServletRequest.getInputStream(),
StandardCharsets.UTF_8);

// Parse into a JsonNode, needed for validation
JsonNode json = this.config.getObjectMapper().readTree(jsonString);

// Validate the Json
Set<ValidationMessage> validationResult = schema.validate(json);
if (validationResult.isEmpty()) {
// Convert the JSON into the object
return this.config.getObjectMapper().treeToValue(json, parameter.getParameterType());
} else {
// Throw the validation exception
throw new ValidateJsonSchemaException(validationResult);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/*
* Copyright 2024 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 com.unitvectory.jsonschema4springboot;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.SpecVersion;
import lombok.Builder;
import lombok.Value;

/**
* The validate JSON Schema configuration.
*
* @author Jared Hatfield (UnitVectorY Labs)
*/
@Value
@Builder
public class ValidateJsonSchemaConfig {

@Builder.Default
private final ObjectMapper objectMapper = new ObjectMapper();

@Builder.Default
private final SpecVersion.VersionFlag specVersion = SpecVersion.VersionFlag.V7;

@Builder.Default
private final JsonSchemaCache cache = new JsonSchemaCacheDefault();

@Builder.Default
private final JsonSchemaLookup lookup = new JsonSchemaLookupDefault();
}
Loading

0 comments on commit b6bda8c

Please sign in to comment.