Skip to content

Commit 61b73ad

Browse files
authored
BAEL-8582 Validate Map<String, String> using Spring Validator (#18499)
1 parent 6306919 commit 61b73ad

File tree

6 files changed

+328
-0
lines changed

6 files changed

+328
-0
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.baeldung.stringmap;
2+
3+
import org.springframework.beans.factory.annotation.Autowired;
4+
import org.springframework.stereotype.Service;
5+
import org.springframework.validation.MapBindingResult;
6+
7+
import java.util.Map;
8+
9+
@Service
10+
public class MapService {
11+
private final MapValidator mapValidator;
12+
13+
@Autowired
14+
public MapService(MapValidator mapValidator) {
15+
this.mapValidator = mapValidator;
16+
}
17+
18+
public void process(Map<String, String> inputMap) {
19+
// Wrap the map in a binding structure for validation
20+
MapBindingResult errors = new MapBindingResult(inputMap, "inputMap");
21+
22+
// Run validation
23+
mapValidator.validate(inputMap, errors);
24+
25+
// Handle validation errors
26+
if (errors.hasErrors()) {
27+
throw new IllegalArgumentException("Validation failed: " + errors.getAllErrors());
28+
}
29+
// Business logic goes here...
30+
}
31+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.baeldung.stringmap;
2+
3+
import org.springframework.stereotype.Service;
4+
import org.springframework.util.StringUtils;
5+
import org.springframework.validation.Errors;
6+
import org.springframework.validation.Validator;
7+
8+
import java.util.Map;
9+
10+
@Service
11+
public class MapValidator implements Validator {
12+
13+
@Override
14+
public boolean supports(Class<?> clazz) {
15+
return Map.class.isAssignableFrom(clazz);
16+
}
17+
18+
@Override
19+
public void validate(Object target, Errors errors) {
20+
Map<?, ?> rawMap = (Map<?, ?>) target;
21+
22+
for (Map.Entry<?, ?> entry : rawMap.entrySet()) {
23+
Object rawKey = entry.getKey();
24+
Object rawValue = entry.getValue();
25+
26+
if (!(rawKey instanceof String) || !(rawValue instanceof String)) {
27+
errors.rejectValue("map[" + rawKey + "]", "map.entry.invalidType", "Map must contain only String keys and values");
28+
continue;
29+
}
30+
31+
String key = (String) rawKey;
32+
String value = (String) rawValue;
33+
34+
// Key validation
35+
if (key.length() < 10) {
36+
errors.rejectValue("map[" + key + "]", "key.tooShort", "Key must be at least 10 characters long");
37+
}
38+
39+
// Value validation
40+
if (!StringUtils.hasText(value)) {
41+
errors.rejectValue("map[" + key + "]", "value.blank", "Value must not be blank");
42+
}
43+
}
44+
}
45+
}
46+
47+
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.baeldung.stringmap;
2+
3+
import org.springframework.boot.SpringApplication;
4+
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
6+
@SpringBootApplication
7+
public class StringMapStarter {
8+
public static void main(String[] args) {
9+
SpringApplication.run(StringMapStarter.class, args);
10+
}
11+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package com.baeldung.stringmap;
2+
3+
import jakarta.validation.constraints.NotBlank;
4+
import org.hibernate.validator.constraints.Length;
5+
6+
import java.util.Map;
7+
import java.util.Objects;
8+
9+
public class WrappedMap {
10+
private Map<@Length(min = 10) String, @NotBlank String> map;
11+
12+
public Map<String, String> getMap() {
13+
return map;
14+
}
15+
16+
public void setMap(Map<String, String> map) {
17+
this.map = map;
18+
}
19+
20+
@Override
21+
public boolean equals(Object o) {
22+
if (o == null || getClass() != o.getClass()) return false;
23+
WrappedMap that = (WrappedMap) o;
24+
return Objects.equals(map, that.map);
25+
}
26+
27+
@Override
28+
public int hashCode() {
29+
return Objects.hashCode(map);
30+
}
31+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.baeldung.stringmap;
2+
3+
import jakarta.validation.ConstraintViolation;
4+
import jakarta.validation.Validation;
5+
import jakarta.validation.Validator;
6+
import jakarta.validation.constraints.NotBlank;
7+
import org.assertj.core.api.Assertions;
8+
import org.hibernate.validator.constraints.Length;
9+
import org.junit.jupiter.api.Test;
10+
11+
import java.util.HashMap;
12+
import java.util.Map;
13+
import java.util.Set;
14+
15+
public class BeanValidationStyleTest {
16+
17+
@Test
18+
void givenInnerVariableMap_whenValidateIsCalled_thenValidationNotWorking() {
19+
Map<@Length(min = 10) String, @NotBlank String> givenMap = new HashMap<>();
20+
givenMap.put("tooShort", "");
21+
22+
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
23+
Set<ConstraintViolation<Map<String, String>>> violations = validator.validate(givenMap);
24+
25+
Assertions.assertThat(violations).isEmpty(); // this shouldn't be empty
26+
}
27+
28+
@Test
29+
void givenWrappedMap_whenValidateIsCalled_thenValidationWorking() {
30+
WrappedMap wrappedMap = new WrappedMap();
31+
Map<String, String> givenMap = new HashMap<>();
32+
givenMap.put("tooShort", "");
33+
wrappedMap.setMap(givenMap);
34+
35+
Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
36+
Set<ConstraintViolation<WrappedMap>> violations = validator.validate(wrappedMap);
37+
38+
Assertions.assertThat(violations).isNotEmpty();
39+
}
40+
41+
42+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
package com.baeldung.stringmap;
2+
3+
import org.junit.jupiter.api.BeforeEach;
4+
import org.junit.jupiter.api.Test;
5+
import org.springframework.validation.Errors;
6+
import org.springframework.validation.MapBindingResult;
7+
8+
import java.util.HashMap;
9+
import java.util.LinkedHashMap;
10+
import java.util.Map;
11+
12+
import static org.assertj.core.api.Assertions.assertThat;
13+
14+
class MapValidatorTest {
15+
private MapValidator mapValidator;
16+
private static final String OBJECT_NAME = "targetMap"; // Consistent name for the binding result
17+
18+
@BeforeEach
19+
void setUp() {
20+
mapValidator = new MapValidator();
21+
}
22+
23+
@Test
24+
void givenMapValidator_whenSupportsCalledWithMapClass_thenReturnsTrue() {
25+
assertThat(mapValidator.supports(Map.class)).isTrue();
26+
assertThat(mapValidator.supports(HashMap.class)).isTrue(); // Also true for subclasses
27+
assertThat(mapValidator.supports(LinkedHashMap.class)).isTrue();
28+
}
29+
30+
@Test
31+
void givenMapValidator_whenSupportsCalledWithNonMapClass_thenReturnsFalse() {
32+
assertThat(mapValidator.supports(String.class)).isFalse();
33+
assertThat(mapValidator.supports(Object.class)).isFalse();
34+
assertThat(mapValidator.supports(Integer.class)).isFalse();
35+
}
36+
37+
@Test
38+
void givenValidMap_whenValidateIsCalled_thenValidationSucceeds() {
39+
Map<String, String> validMap = new LinkedHashMap<>();
40+
validMap.put("longEnoughKey1", "Valid Value 1");
41+
validMap.put("anotherValidKeyHere", "Valid Value 2");
42+
Errors errors = new MapBindingResult(new HashMap<>(), OBJECT_NAME);
43+
44+
mapValidator.validate(validMap, errors);
45+
46+
assertThat(errors.hasErrors()).isFalse();
47+
}
48+
49+
@Test
50+
void givenMapWithNonStringKey_whenValidateIsCalled_thenRejectsWithInvalidTypeError() {
51+
Map<Object, Object> mapWithInvalidKey = new LinkedHashMap<>();
52+
mapWithInvalidKey.put(123, "Value for integer key"); // Invalid key type
53+
mapWithInvalidKey.put("validKeyString", "Valid Value");
54+
Errors errors = new MapBindingResult(new HashMap<>(), OBJECT_NAME);
55+
56+
mapValidator.validate(mapWithInvalidKey, errors);
57+
58+
assertThat(errors.hasErrors()).isTrue();
59+
assertThat(errors.getErrorCount()).isEqualTo(1);
60+
assertThat(errors.getFieldError("map[123]")).isNotNull();
61+
assertThat(errors.getFieldError("map[123]").getCode()).isEqualTo("map.entry.invalidType");
62+
}
63+
64+
@Test
65+
void givenMapWithNonStringValue_whenValidateIsCalled_thenRejectsWithInvalidTypeError() {
66+
Map<Object, Object> mapWithInvalidValue = new LinkedHashMap<>();
67+
mapWithInvalidValue.put("validKeyString1", 12345); // Invalid value type
68+
mapWithInvalidValue.put("validKeyString2", "Valid Value");
69+
Errors errors = new MapBindingResult(new HashMap<>(), OBJECT_NAME);
70+
71+
mapValidator.validate(mapWithInvalidValue, errors);
72+
73+
assertThat(errors.hasErrors()).isTrue();
74+
assertThat(errors.getErrorCount()).isEqualTo(1);
75+
assertThat(errors.getFieldError("map[validKeyString1]")).isNotNull();
76+
assertThat(errors.getFieldError("map[validKeyString1]").getCode()).isEqualTo("map.entry.invalidType");
77+
}
78+
79+
@Test
80+
void givenMapWithShortKey_whenValidateIsCalled_thenRejectsWithKeyTooShortError() {
81+
Map<String, String> mapWithShortKey = new LinkedHashMap<>();
82+
mapWithShortKey.put("shortKey", "Valid Value"); // Key too short
83+
mapWithShortKey.put("longEnoughKey1", "Another Valid Value");
84+
Errors errors = new MapBindingResult(new HashMap<>(), OBJECT_NAME);
85+
86+
mapValidator.validate(mapWithShortKey, errors);
87+
88+
assertThat(errors.hasErrors()).isTrue();
89+
assertThat(errors.getErrorCount()).isEqualTo(1);
90+
assertThat(errors.getFieldError("map[shortKey]")).isNotNull();
91+
assertThat(errors.getFieldError("map[shortKey]").getCode()).isEqualTo("key.tooShort");
92+
}
93+
94+
@Test
95+
void givenMapWithBlankValue_whenValidateIsCalled_thenRejectsWithValueBlankError() {
96+
Map<String, String> mapWithBlankValue = new LinkedHashMap<>();
97+
mapWithBlankValue.put("longEnoughKey1", ""); // Blank value
98+
mapWithBlankValue.put("longEnoughKey2", " "); // Blank value (whitespace)
99+
mapWithBlankValue.put("longEnoughKey3", "Valid Value");
100+
Errors errors = new MapBindingResult(new HashMap<>(), OBJECT_NAME);
101+
102+
mapValidator.validate(mapWithBlankValue, errors);
103+
104+
// Then: Errors should be reported for both blank values
105+
assertThat(errors.hasErrors()).isTrue();
106+
assertThat(errors.getErrorCount()).isEqualTo(2);
107+
assertThat(errors.getFieldError("map[longEnoughKey1]")).isNotNull();
108+
assertThat(errors.getFieldError("map[longEnoughKey1]").getCode()).isEqualTo("value.blank");
109+
assertThat(errors.getFieldError("map[longEnoughKey2]")).isNotNull();
110+
assertThat(errors.getFieldError("map[longEnoughKey2]").getCode()).isEqualTo("value.blank");
111+
}
112+
113+
@Test
114+
void givenMapWithNullValue_whenValidateIsCalled_thenRejectsWithInvalidTypeError() {
115+
Map<String, Object> mapWithNullValue = new LinkedHashMap<>();
116+
mapWithNullValue.put("longEnoughKey1", null); // Null value (will fail type check first)
117+
mapWithNullValue.put("longEnoughKey2", "Valid Value");
118+
Errors errors = new MapBindingResult(new HashMap<>(), OBJECT_NAME);
119+
120+
mapValidator.validate(mapWithNullValue, errors);
121+
122+
assertThat(errors.hasErrors()).isTrue();
123+
assertThat(errors.getErrorCount()).isEqualTo(1);
124+
assertThat(errors.getFieldError("map[longEnoughKey1]")).isNotNull();
125+
assertThat(errors.getFieldError("map[longEnoughKey1]").getCode()).isEqualTo("map.entry.invalidType");
126+
}
127+
128+
@Test
129+
void givenMapWithMultipleValidationIssues_whenValidateIsCalled_thenReportsAllErrors() {
130+
Map<Object, Object> mapWithMultipleErrors = new LinkedHashMap<>();
131+
mapWithMultipleErrors.put("short", "Valid Value"); // Short key
132+
mapWithMultipleErrors.put("longEnoughKey1", ""); // Blank value
133+
mapWithMultipleErrors.put(123, "Value"); // Invalid key type
134+
mapWithMultipleErrors.put("longEnoughKey2", 456); // Invalid value type
135+
mapWithMultipleErrors.put("anotherValidKeyHere", "Good Value"); // Valid entry
136+
Errors errors = new MapBindingResult(new HashMap<>(), OBJECT_NAME);
137+
138+
mapValidator.validate(mapWithMultipleErrors, errors);
139+
140+
assertThat(errors.hasErrors()).isTrue();
141+
assertThat(errors.getErrorCount()).isEqualTo(4);
142+
143+
// Check specific errors
144+
assertThat(errors.getFieldError("map[short]")).isNotNull();
145+
assertThat(errors.getFieldError("map[short]").getCode()).isEqualTo("key.tooShort");
146+
147+
assertThat(errors.getFieldError("map[longEnoughKey1]")).isNotNull();
148+
assertThat(errors.getFieldError("map[longEnoughKey1]").getCode()).isEqualTo("value.blank");
149+
150+
assertThat(errors.getFieldError("map[123]")).isNotNull();
151+
assertThat(errors.getFieldError("map[123]").getCode()).isEqualTo("map.entry.invalidType");
152+
153+
assertThat(errors.getFieldError("map[longEnoughKey2]")).isNotNull();
154+
assertThat(errors.getFieldError("map[longEnoughKey2]").getCode()).isEqualTo("map.entry.invalidType");
155+
}
156+
157+
@Test
158+
void givenEmptyMap_whenValidateIsCalled_thenValidationSucceeds() {
159+
Map<String, String> emptyMap = new HashMap<>();
160+
Errors errors = new MapBindingResult(new HashMap<>(), OBJECT_NAME);
161+
162+
mapValidator.validate(emptyMap, errors);
163+
164+
assertThat(errors.hasErrors()).isFalse();
165+
}
166+
}

0 commit comments

Comments
 (0)