Skip to content

Commit e39bba9

Browse files
committed
lenient mode support / parameterizing request body validation config
1 parent 4ef45b1 commit e39bba9

16 files changed

Lines changed: 182 additions & 57 deletions

File tree

kappa-servlet-adapter/src/main/java/com/github/erosb/kappa/operation/validator/adapters/server/servlet/OpenApiBasedRequestValidationFilter.java

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import com.fasterxml.jackson.databind.ObjectMapper;
44
import com.fasterxml.jackson.databind.node.ArrayNode;
55
import com.fasterxml.jackson.databind.node.ObjectNode;
6+
import com.github.erosb.jsonsKema.ValidatorConfig;
67
import com.github.erosb.kappa.core.validation.OpenApiValidationFailure;
78
import com.github.erosb.kappa.core.validation.ValidationException;
89
import com.github.erosb.kappa.operation.validator.model.Request;
@@ -27,23 +28,43 @@ public static OpenApiBasedRequestValidationFilter forApiDescription(
2728
OpenApi3 api,
2829
ValidationFailureSender validationFailureSender
2930
) {
30-
return new OpenApiBasedRequestValidationFilter(path -> api, validationFailureSender);
31+
return new OpenApiBasedRequestValidationFilter(path -> api, validationFailureSender, ValidatorConfig.builder().build());
32+
}
33+
34+
public static OpenApiBasedRequestValidationFilter forApiDescription(
35+
OpenApi3 api,
36+
ValidationFailureSender validationFailureSender,
37+
ValidatorConfig requestBodyValidatorConfig
38+
) {
39+
return new OpenApiBasedRequestValidationFilter(path -> api, validationFailureSender, requestBodyValidatorConfig);
3140
}
3241

3342
public static OpenApiBasedRequestValidationFilter forApiLookup(
3443
OpenApiLookup lookupFn,
3544
ValidationFailureSender validationFailureSender
3645
) {
37-
return new OpenApiBasedRequestValidationFilter(lookupFn, validationFailureSender);
46+
return forApiLookup(lookupFn, validationFailureSender, ValidatorConfig.builder().build());
47+
}
48+
49+
50+
public static OpenApiBasedRequestValidationFilter forApiLookup(
51+
OpenApiLookup lookupFn,
52+
ValidationFailureSender validationFailureSender,
53+
ValidatorConfig requestBodyValidatorConfig
54+
) {
55+
return new OpenApiBasedRequestValidationFilter(lookupFn, validationFailureSender, requestBodyValidatorConfig);
3856
}
3957

4058
private final OpenApiLookup lookupFn;
59+
private final ValidatorConfig requestBodyValidatorConfig;
4160

4261
private final ValidationFailureSender validationFailureSender;
4362

44-
private OpenApiBasedRequestValidationFilter(OpenApiLookup lookupFn, ValidationFailureSender validationFailureSender) {
63+
private OpenApiBasedRequestValidationFilter(OpenApiLookup lookupFn, ValidationFailureSender validationFailureSender,
64+
ValidatorConfig requestBodyValidatorConfig) {
4565
this.lookupFn = requireNonNull(lookupFn);
4666
this.validationFailureSender = requireNonNull(validationFailureSender);
67+
this.requestBodyValidatorConfig = requireNonNull(requestBodyValidatorConfig);
4768
}
4869

4970
@Override
@@ -80,7 +101,7 @@ public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain
80101

81102
if (api != null) {
82103
// we do the validation
83-
new RequestValidator(api).validate(jakartaRequest);
104+
new RequestValidator(api, requestBodyValidatorConfig).validate(jakartaRequest);
84105

85106
// if no request validation error was found, we proceed with the request execution
86107
chain.doFilter(memoizedReq, httpResp);

kappa-servlet-adapter/src/test/java/com/github/erosb/kappa/operation/validator/adapters/server/servlet/OpenApiBasedRequestValidationFilterTest.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.github.erosb.kappa.operation.validator.adapters.server.servlet;
22

3+
import com.github.erosb.jsonsKema.ValidatorConfig;
34
import com.github.erosb.kappa.core.exception.ResolutionException;
45
import com.github.erosb.kappa.core.validation.ValidationException;
56
import com.github.erosb.kappa.parser.OpenApi3Parser;
@@ -32,7 +33,9 @@ public class OpenApiBasedRequestValidationFilterTest {
3233

3334
@Test
3435
public void bodySchemaFailure() throws Exception {
35-
OpenApiBasedRequestValidationFilter filter = forApiDescription(usersApi, ValidationFailureSender.defaultSender());
36+
OpenApiBasedRequestValidationFilter filter = forApiDescription(usersApi,
37+
ValidationFailureSender.defaultSender()
38+
);
3639
HttpServletResponse resp = mock(HttpServletResponse.class);
3740
ByteArrayOutputStream out = new ByteArrayOutputStream();
3841
PrintWriter pw = new PrintWriter(out);
@@ -55,7 +58,10 @@ public void bodySchemaFailure() throws Exception {
5558

5659
@Test
5760
public void multipleApiYamlLookup() throws Exception {
58-
OpenApiBasedRequestValidationFilter filter = forApiLookup(path -> usersApi, ValidationFailureSender.defaultSender());
61+
OpenApiBasedRequestValidationFilter filter = forApiLookup(
62+
path -> usersApi,
63+
ValidationFailureSender.defaultSender()
64+
);
5965
HttpServletResponse resp = mock(HttpServletResponse.class);
6066
ByteArrayOutputStream out = new ByteArrayOutputStream();
6167
PrintWriter pw = new PrintWriter(out);

kappa-spring/src/main/java/com/github/erosb/kappa/autoconfigure/KappaConfiguration.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ public FilterRegistrationBean<OpenApiBasedRequestValidationFilter> openApiBasedR
2020
}
2121
OpenApiBasedRequestValidationFilter filter = OpenApiBasedRequestValidationFilter.forApiLookup(
2222
new PathPatternMatchingOpenApiLookup(configuration),
23-
configuration.getValidationFailureSender()
23+
configuration.getValidationFailureSender(),
24+
configuration.getRequestBodyValidatorConfig()
2425
);
2526
FilterRegistrationBean<OpenApiBasedRequestValidationFilter> registration = new FilterRegistrationBean<>();
2627
registration.setFilter(filter);

kappa-spring/src/main/java/com/github/erosb/kappa/autoconfigure/KappaSpringConfiguration.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.github.erosb.kappa.autoconfigure;
22

3+
import com.github.erosb.jsonsKema.ValidatorConfig;
34
import com.github.erosb.kappa.operation.validator.adapters.server.servlet.ValidationFailureSender;
45

56
import java.util.LinkedHashMap;
@@ -12,6 +13,8 @@ public class KappaSpringConfiguration {
1213

1314
private List<String> ignoredPathPatterns = List.of();
1415

16+
private ValidatorConfig requestBodyValidatorConfig = ValidatorConfig.builder().build();
17+
1518
public LinkedHashMap<String, String> getOpenapiDescriptions() {
1619
return openapiDescriptions;
1720
}
@@ -36,4 +39,12 @@ public void setIgnoredPathPatterns(String... whitelistPathPatterns) {
3639
public List<String> getIgnoredPathPatterns() {
3740
return ignoredPathPatterns;
3841
}
42+
43+
public ValidatorConfig getRequestBodyValidatorConfig() {
44+
return requestBodyValidatorConfig;
45+
}
46+
47+
public void setRequestBodyValidatorConfig(ValidatorConfig requestBodyValidatorConfig) {
48+
this.requestBodyValidatorConfig = requestBodyValidatorConfig;
49+
}
3950
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package com.github.erosb.kappa.lenienttest;
2+
3+
import com.github.erosb.jsonsKema.PrimitiveValidationStrategy;
4+
import com.github.erosb.jsonsKema.ValidatorConfig;
5+
import com.github.erosb.kappa.autoconfigure.EnableKappaRequestValidation;
6+
import com.github.erosb.kappa.autoconfigure.KappaSpringConfiguration;
7+
import org.springframework.boot.SpringApplication;
8+
import org.springframework.boot.autoconfigure.SpringBootApplication;
9+
import org.springframework.context.annotation.Bean;
10+
11+
import java.util.LinkedHashMap;
12+
13+
@SpringBootApplication
14+
@EnableKappaRequestValidation
15+
public class LenientApplication {
16+
17+
18+
public static void main(String[] args) {
19+
SpringApplication.run(LenientApplication.class);
20+
}
21+
22+
@Bean
23+
public KappaSpringConfiguration kappaSpringConfiguration() {
24+
KappaSpringConfiguration kappaConfig = new KappaSpringConfiguration();
25+
var pathPatternToOpenapiDescription = new LinkedHashMap<String, String>();
26+
pathPatternToOpenapiDescription.put("/**", "/openapi/users-api.yaml");
27+
kappaConfig.setOpenapiDescriptions(pathPatternToOpenapiDescription);
28+
kappaConfig.setRequestBodyValidatorConfig(ValidatorConfig.builder()
29+
.primitiveValidationStrategy(PrimitiveValidationStrategy.LENIENT)
30+
.build()
31+
);
32+
return kappaConfig;
33+
}
34+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package com.github.erosb.kappa.lenienttest;
2+
3+
import org.junit.Test;
4+
import org.junit.runner.RunWith;
5+
import org.springframework.beans.factory.annotation.Autowired;
6+
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
7+
import org.springframework.boot.test.context.SpringBootTest;
8+
import org.springframework.http.MediaType;
9+
import org.springframework.test.context.ActiveProfiles;
10+
import org.springframework.test.context.junit4.SpringRunner;
11+
import org.springframework.test.web.servlet.MockMvc;
12+
13+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
14+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
15+
16+
@RunWith(SpringRunner.class)
17+
@SpringBootTest()
18+
@ActiveProfiles("test")
19+
@AutoConfigureMockMvc
20+
public class LenientBodyValidationTest {
21+
22+
@Autowired
23+
MockMvc mvc;
24+
25+
@Test
26+
public void lenientModeWorks() throws Exception {
27+
mvc.perform(post("/users")
28+
.contentType(MediaType.APPLICATION_JSON)
29+
// age should not be null as per the schema, but in lenient mode it is accepted
30+
// since it is an optional property
31+
.content("""
32+
{
33+
"name": "John Doe",
34+
"email": "johndoe@example.org",
35+
"age": null
36+
}
37+
"""))
38+
.andExpect(status().isCreated());
39+
}
40+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package com.github.erosb.kappa.lenienttest;
2+
3+
import org.springframework.http.HttpStatus;
4+
import org.springframework.web.bind.annotation.PostMapping;
5+
import org.springframework.web.bind.annotation.RequestBody;
6+
import org.springframework.web.bind.annotation.RequestMapping;
7+
import org.springframework.web.bind.annotation.ResponseStatus;
8+
import org.springframework.web.bind.annotation.RestController;
9+
10+
class CreateUserRequest {
11+
private String name;
12+
private String email;
13+
private Integer age;
14+
}
15+
16+
@RestController
17+
@RequestMapping("/users")
18+
public class UsersController {
19+
20+
@PostMapping
21+
@ResponseStatus(HttpStatus.CREATED)
22+
void createUser(@RequestBody CreateUserRequest request) {
23+
24+
}
25+
}

kappa-spring/src/test/resources/openapi/users/schemas.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
"email": {
1616
"type": "string",
1717
"format": "email"
18+
},
19+
"age": {
20+
"type": "integer"
1821
}
1922
}
2023
}

src/main/java/com/github/erosb/kappa/operation/validator/validation/BodyValidator.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import com.fasterxml.jackson.databind.node.JsonNodeFactory;
44
import com.github.erosb.jsonsKema.IJsonValue;
5+
import com.github.erosb.jsonsKema.JsonNull;
56
import com.github.erosb.jsonsKema.JsonParseException;
7+
import com.github.erosb.jsonsKema.ValidatorConfig;
68
import com.github.erosb.kappa.core.model.v3.OAI3;
79
import com.github.erosb.kappa.core.validation.OpenApiValidationFailure;
810
import com.github.erosb.kappa.core.validation.OperationContextUriFactory;
@@ -29,18 +31,19 @@ class BodyValidator {
2931

3032
void validate(final Body body,
3133
final String rawContentType,
32-
final ValidationData<?> validation) {
34+
final ValidationData<?> validation,
35+
ValidatorConfig requestBodyValidatorConfig) {
3336

3437
if (validator == null) {
3538
return; // No schema specified for body
3639
} else if (body == null) {
37-
validator.validate(JsonNodeFactory.instance.nullNode(), uriFactory.httpEntity(), validation);
40+
validator.validate(new JsonNull(), validation, requestBodyValidatorConfig);
3841
return;
3942
}
4043

4144
try {
4245
IJsonValue jsonBody = body.contentAsNode(rawContentType, uriFactory.httpEntity());
43-
validator.validate(jsonBody, validation);
46+
validator.validate(jsonBody, validation, requestBodyValidatorConfig);
4447
} catch (JsonParseException ex) {
4548
validation.add(OpenApiValidationFailure.unparseableHttpEntity(ex, uriFactory.definitionHttpEntity()));
4649
}

src/main/java/com/github/erosb/kappa/operation/validator/validation/OperationValidator.java

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
package com.github.erosb.kappa.operation.validator.validation;
22

3-
import com.fasterxml.jackson.databind.JsonNode;
43
import com.github.erosb.jsonsKema.IJsonValue;
54
import com.github.erosb.jsonsKema.SourceLocation;
5+
import com.github.erosb.jsonsKema.ValidatorConfig;
66
import com.github.erosb.kappa.core.exception.DecodeException;
77
import com.github.erosb.kappa.core.model.v3.OAI3;
88
import com.github.erosb.kappa.core.validation.OpenApiValidationFailure;
99
import com.github.erosb.kappa.core.validation.OperationContextUriFactory;
10-
import com.github.erosb.kappa.core.validation.URIFactory;
1110
import com.github.erosb.kappa.operation.validator.model.Request;
1211
import com.github.erosb.kappa.operation.validator.model.Response;
1312
import com.github.erosb.kappa.operation.validator.model.impl.Body;
@@ -69,6 +68,7 @@ public class OperationValidator {
6968
private final Operation operation;
7069
private final String templatePath;
7170
private final List<Pattern> pathPatterns;
71+
private final ValidatorConfig validatorConfig;
7272

7373
/**
7474
* Creates a validator for the given operation.
@@ -78,7 +78,7 @@ public class OperationValidator {
7878
* @param operation The Operation to validate.
7979
*/
8080
public OperationValidator(final OpenApi3 openApi, final Path path, final Operation operation) {
81-
this(null, openApi, path, operation);
81+
this(null, openApi, path, operation, ValidatorConfig.builder().build());
8282
}
8383

8484
/**
@@ -92,12 +92,14 @@ public OperationValidator(final OpenApi3 openApi, final Path path, final Operati
9292
OperationValidator(final List<Pattern> pathPatterns,
9393
final OpenApi3 openApi,
9494
final Path path,
95-
final Operation operation) {
95+
final Operation operation,
96+
ValidatorConfig validatorConfig) {
9697

9798
this.context = new ValidationContext<>(openApi.getContext(), openApi.getPathFrom(path),
9899
path.findHttpMethodByOperation(operation));
99100
requireNonNull(operation, OPERATION_REQUIRED_ERR_MSG);
100101
this.templatePath = openApi.getPathFrom(requireNonNull(path, PATH_REQUIRED_ERR_MSG));
102+
this.validatorConfig = validatorConfig;
101103

102104
// Clone operation
103105
this.operation = buildFlatOperation(operation);
@@ -265,7 +267,8 @@ public void validateBody(final Request request, final ValidationData<?> validati
265267
request.getContentType(),
266268
request.getBody(),
267269
validation,
268-
context.requestScopedUriFactory().definitionHttpEntity()
270+
context.requestScopedUriFactory().definitionHttpEntity(),
271+
validatorConfig
269272
);
270273
}
271274

@@ -317,15 +320,17 @@ public void validateBody(final Response response,
317320
response.getContentType(),
318321
response.getBody(),
319322
validation,
320-
context.responseScopedUriFactory(String.valueOf(response.getStatus())).definitionHttpEntity()
323+
context.responseScopedUriFactory(String.valueOf(response.getStatus())).definitionHttpEntity(),
324+
ValidatorConfig.builder().build()
321325
);
322326
}
323327

324328
private void validateBodyWithContentType(final Map<MediaTypeContainer, BodyValidator> validators,
325329
final String rawContentType,
326330
final Body body,
327331
final ValidationData<?> validation,
328-
SourceLocation bodyDefinitionLocation) {
332+
SourceLocation bodyDefinitionLocation,
333+
ValidatorConfig validatorConfig) {
329334

330335
final MediaTypeContainer contentType = MediaTypeContainer.create(rawContentType);
331336

@@ -343,7 +348,8 @@ private void validateBodyWithContentType(final Map<MediaTypeContainer, BodyValid
343348

344349
validator.validate(body,
345350
rawContentType,
346-
validation);
351+
validation,
352+
validatorConfig);
347353
}
348354

349355
/**

0 commit comments

Comments
 (0)