Skip to content

Commit 0afd3a7

Browse files
committed
feat(validate): Object validation with required/properties/additionalProperties
1 parent 4c3f158 commit 0afd3a7

2 files changed

Lines changed: 113 additions & 1 deletion

File tree

src/main/java/com/retailsvc/http/validate/DefaultValidator.java

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.retailsvc.http.validate;
22

33
import com.retailsvc.http.ValidationException;
4+
import com.retailsvc.http.spec.schema.AdditionalProperties;
45
import com.retailsvc.http.spec.schema.AllOfSchema;
56
import com.retailsvc.http.spec.schema.AnyOfSchema;
67
import com.retailsvc.http.spec.schema.ArraySchema;
@@ -19,6 +20,7 @@
1920
import com.retailsvc.http.spec.schema.TypeName;
2021
import java.time.LocalDate;
2122
import java.time.OffsetDateTime;
23+
import java.util.Map;
2224
import java.util.Objects;
2325
import java.util.UUID;
2426
import java.util.function.Function;
@@ -154,8 +156,43 @@ else if (value instanceof String str) {
154156
fail(pointer, "multipleOf", "not a multiple of " + s.multipleOf(), n);
155157
}
156158

159+
@SuppressWarnings("unchecked")
157160
private void validateObject(Object value, ObjectSchema s, String pointer) {
158-
throw new UnsupportedOperationException("E4 implements object");
161+
require(value instanceof Map, pointer, "type", "expected object");
162+
Map<String, Object> map = (Map<String, Object>) value;
163+
164+
for (String required : s.required()) {
165+
require(
166+
map.containsKey(required),
167+
pointer + "/" + required,
168+
"required",
169+
"required property missing");
170+
}
171+
172+
if (s.minProperties() != null && map.size() < s.minProperties())
173+
fail(pointer, "minProperties", "fewer than " + s.minProperties() + " properties", map.size());
174+
if (s.maxProperties() != null && map.size() > s.maxProperties())
175+
fail(pointer, "maxProperties", "more than " + s.maxProperties() + " properties", map.size());
176+
177+
for (var entry : map.entrySet()) {
178+
String childPointer = pointer + "/" + entry.getKey();
179+
Schema propSchema = s.properties().get(entry.getKey());
180+
if (propSchema != null) {
181+
validate(entry.getValue(), propSchema, childPointer);
182+
} else {
183+
switch (s.additionalProperties()) {
184+
case AdditionalProperties.Allowed a -> {}
185+
case AdditionalProperties.Forbidden f ->
186+
fail(
187+
childPointer,
188+
"additionalProperties",
189+
"additional property not allowed",
190+
entry.getKey());
191+
case AdditionalProperties.SchemaConstraint sc ->
192+
validate(entry.getValue(), sc.schema(), childPointer);
193+
}
194+
}
195+
}
159196
}
160197

161198
private void validateArray(Object value, ArraySchema s, String pointer) {
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package com.retailsvc.http.validate;
2+
3+
import static org.assertj.core.api.Assertions.assertThatCode;
4+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
5+
6+
import com.retailsvc.http.ValidationException;
7+
import com.retailsvc.http.spec.schema.AdditionalProperties;
8+
import com.retailsvc.http.spec.schema.ObjectSchema;
9+
import com.retailsvc.http.spec.schema.Schema;
10+
import com.retailsvc.http.spec.schema.StringSchema;
11+
import com.retailsvc.http.spec.schema.TypeName;
12+
import java.util.List;
13+
import java.util.Map;
14+
import java.util.Set;
15+
import org.junit.jupiter.api.Test;
16+
17+
class ObjectValidationTest {
18+
private final Validator v =
19+
new DefaultValidator(
20+
name -> {
21+
throw new AssertionError();
22+
});
23+
24+
private ObjectSchema obj(
25+
Map<String, Schema> props, List<String> required, AdditionalProperties ap) {
26+
return new ObjectSchema(Set.of(TypeName.OBJECT), props, required, ap, null, null);
27+
}
28+
29+
@Test
30+
void requiredFieldMissing() {
31+
var s =
32+
obj(
33+
Map.of("name", new StringSchema(Set.of(TypeName.STRING), null, null, null, null, null)),
34+
List.of("name"),
35+
new AdditionalProperties.Allowed());
36+
assertThatThrownBy(() -> v.validate(Map.of(), s, ""))
37+
.isInstanceOf(ValidationException.class)
38+
.extracting(t -> ((ValidationException) t).error().keyword())
39+
.isEqualTo("required");
40+
}
41+
42+
@Test
43+
void propertyValidatedAtPointer() {
44+
var s =
45+
obj(
46+
Map.of("name", new StringSchema(Set.of(TypeName.STRING), null, 3, null, null, null)),
47+
List.of(),
48+
new AdditionalProperties.Allowed());
49+
assertThatThrownBy(() -> v.validate(Map.of("name", "ab"), s, ""))
50+
.extracting(t -> ((ValidationException) t).error().pointer())
51+
.isEqualTo("/name");
52+
}
53+
54+
@Test
55+
void additionalPropertiesAllowedByDefault() {
56+
var s = obj(Map.of(), List.of(), new AdditionalProperties.Allowed());
57+
assertThatCode(() -> v.validate(Map.of("extra", "x"), s, "")).doesNotThrowAnyException();
58+
}
59+
60+
@Test
61+
void additionalPropertiesForbidden() {
62+
var s = obj(Map.of(), List.of(), new AdditionalProperties.Forbidden());
63+
assertThatThrownBy(() -> v.validate(Map.of("extra", "x"), s, ""))
64+
.extracting(t -> ((ValidationException) t).error().keyword())
65+
.isEqualTo("additionalProperties");
66+
}
67+
68+
@Test
69+
void rejectsNonObject() {
70+
var s = obj(Map.of(), List.of(), new AdditionalProperties.Allowed());
71+
assertThatThrownBy(() -> v.validate("nope", s, "/v"))
72+
.extracting(t -> ((ValidationException) t).error().keyword())
73+
.isEqualTo("type");
74+
}
75+
}

0 commit comments

Comments
 (0)