3 min read

Spring Boot ft. JsonSchema - Payload validation

Spring Boot ft. JsonSchema - Payload validation
#springboot #gradle #json
When big payloads are coming in, they should be validated and should be validated right. (Example made using Spring Boot + Gradle)

Brief

Let's think of a service that calculates taxes based on a massive input (person details, assets details, etc.).

public float calculate(MassivePayload massivePayload) {
	if (massivePayload.getPerson()!=null) {
		if (massivePayload.getPerson.getHouse()!=null) {
			if(massivePayload.getPerson.getHouse().getType()!=null) {
				return getTaxForHouse(massivePayload.getPerson.getHouse().getType());
        	}
    	}
	}
}
private float getTaxForHouse(HouseType houseType) {
	...
}

The code snippet above should theoretically work but it's a nightmare to read and to add or change constraints.

We should separate the concerns:

  1. Validate payload
  2. Run business logic

Payload validation is made easy by javax.validation.constraints. It has a lot of annotation based constraints that when ran through a validator, they get triggered and return a set of ConstraintViolations in case of an invalid input. So, we can have some conditions added in this way:

public class MassivePayload {
	@NotNull(message = "Person cannot be null")
	private Person person;
    
    ...
    
    public Person getPerson(){
    	return person;
    }
}

And the code in the method can be changed like this:

public float calculateTax(MassivePayload massivePayload) {
	ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
	Validator validator = factory.getValidator();
    	Set<ConstraintViolation<MassivePayload>> violations = validator.validate(massivePayload);
    	if(violations.isEmpty()){
    	...
    	}
}

In case of a Controller method, validation can be done even more straightforward adding the @Valid annotation next to the payload body.

@RestController("/tax")
public class TaxController{

	@Post("/calculate")
	public float calculateTax(@Valid @Body MassiveInput massiveInput) {
    	...
    }
}

This is a better approach but still, on a big object with lots of fields and conditions, constraint management can still get cumbersome, enter JsonSchema.


Finding

JsonSchema is a grammar language for defining the structure, content, and (to some extent) semantics of JSON objects. It lets you specify metadata (data about data) about what an object's properties mean and what values are valid for those properties.

Example:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "title": "Massive Input with details about a person ",
  "type": "object",
  "required": ["person"],
  "properties": {
    "person": {
      "type": "object",
      "required": ["lastName"],
      "properties": {
        "firstName": {
          "type": "string"
        },
        "lastName": {
          "type": "string"
        }
      }
    }
  }
}

This example is written using json schema draft-07 version. You can read more about different versions and features here: https://json-schema.org/specification.html

Library

Multiple Java implementations can be found for JsonSchema validation: https://json-schema.org/implementations.html#from-code

In this example, I used com.networknt.json-schema-validator:1.0.59.

The library needs a JsonSchema file to validate incoming Json payloads against. In this example it is placed inside the jar's resource files, but it can be brought in from a remote location too. More details regarding this library can be found here: https://github.com/networknt/json-schema-validator

Configuration

@Configuration
public class JsonSchemaConfiguration {
    private static final String SCHEMA_LOCATION = "/jsonSchema.json";
    private final ObjectMapper mapper = new ObjectMapper();

    @Bean
    public JsonSchema getJsonSchema() throws IOException {
     JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);

        InputStream inputStream = getClass().getResourceAsStream(SCHEMA_LOCATION);
        JsonNode schemaJson = objectMapper.readTree(inputStream);

        JsonSchema jsonSchema = factory.getSchema(schemaJson);
        jsonSchema.initializeValidators();
        return jsonSchema;
    }
}

Usage

The library accepts a JsonNode object as an input and will return a set of ConstraintViolationError when the incoming payload fails to meet one or more conditions specified in the JsonSchema file.

After validation, the result can be mapped to a custom object and returned or thrown as an exception.

@Service
@RequiredArgsConstructor
public class JsonSchemaValidator {

    private final JsonSchema jsonSchema;

    public ValidationResult validate(JsonNode jsonNode) {

        Set<ConstraintViolationError> validationErrors = jsonSchema.validate(jsonNode);

        return ValidationResult.builder()
                .valid(validationErrors.isEmpty())
                .errors(validationErrors.stream()
                    .map(result -> ValidationError.builder()
                        .field(result.getPath())
                        .message(result.getMessage())
                        .build())
                    .collect(Collectors.toSet()))
                .build();
    }
}

In the end, this JsonSchemaValidator service can be injected in a Filter before it actually reaches the Controller class and make sure the payload is fine when it's time to run business logic.

Bonus

When working with complex json schemas, the clean way to manage them is to, instead of having one big schema with all the object definitions inside, split it into different files containing an object definition each.

The library can handle working with complex json schemas that refer other schemas.

In order for this to work, the schemas need to have the $id property set with the file location so it can be picked up by other files which refer to it.

Example

{
  "$id": "classpath:/json1.json",
  "type": "object",
  "properties": {
    "field1": {
      "type": "string"
    },
    "field2": {
      "type": "string"
    }
  "$schema": "http://json-schema.org/draft-07/schema#"
}
{
  "$id": "classpath:/json2.json",
  "type": "object",
  "properties": {
    "field1": {
      "$ref": "json1.json"
    }
  }
  "$schema": "http://json-schema.org/draft-07/schema#"
}

💡
Don't miss out on more posts like this! Susbcribe to our free newsletter!
💡
Currently I am working on a Java Interview e-book designed to successfully get you through any Java technical interview you may take.
Stay tuned! 🚀