5 Practical Ways to Handle Dynamic JSON Requests in Spring Boot with Validation
This article explores five practical approaches for processing dynamic JSON payloads in Spring Boot 3.5—using Map, JsonNode, ObjectNode, @JsonAnySetter, and Jackson polymorphic deserialization—detailing code examples, advantages, drawbacks, suitable scenarios, and how to apply runtime validation with networknt’s JSON Schema validator.
Environment: Spring Boot 3.5.0
1. Introduction
In typical development each API endpoint is bound to a fixed DTO class, allowing the framework to map incoming JSON to a strongly‑typed Java object and to perform field‑level validation with @Valid. When the JSON structure varies because of user configuration, device type, or third‑party changes, a static DTO quickly becomes unmanageable. To cope with such uncertainty we need dynamic JSON handling strategies.
2. Solutions
2.1 Use Map<String, Object>
If validation is not required and you simply need all key‑value pairs, map the request body directly to a Map:
@PostMapping("/user")
public ResponseEntity<String> createUser(@RequestBody Map<String, Object> body) {
String name = (String) body.get("name");
Integer age = (Integer) body.get("age");
return ResponseEntity.ok("name: %s, age: %s".formatted(name, age));
}Highly flexible; can handle any dynamic JSON.
Drawback: No compile‑time type safety and you must parse values manually.
Suitable for small tools, internal APIs, or quick prototypes.
2.2 Use JsonNode
JsonNodeis convenient when only part of the structure is known. It lets you read fields dynamically while preserving the JSON tree.
@PostMapping("/user")
public ResponseEntity<String> createUser(@RequestBody JsonNode body) {
String name = body.get("name").asText();
int age = body.has("age") ? body.get("age").asInt() : 0;
return ResponseEntity.ok("name: %s, age: %s".formatted(name, age));
}Can read any field at runtime.
Avoids strict deserialization errors.
Supports tree navigation.
Drawback: Code becomes verbose for deeply nested structures.
Best for APIs that need to be tolerant of version changes or when only a subset of fields is required.
2.3 Use ObjectNode
When you need to modify or merge JSON, ObjectNode provides mutable operations:
private final ObjectMapper mapper;
@PostMapping("/merge")
public ResponseEntity<ObjectNode> mergeData(@RequestBody ObjectNode data) {
ObjectNode extra = mapper.createObjectNode();
extra.put("createTime", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").format(LocalDateTime.now()));
extra.put("createBy", "pack_xg");
data.setAll(extra);
return ResponseEntity.ok(data);
}Full control over dynamic JSON.
Ideal for data transformation or merge APIs.
Drawback: Requires manual handling logic.
Use case: building composite responses or enriching incoming payloads.
2.4 Use @JsonAnySetter
When a JSON object has a few fixed fields (e.g., id, name) and an arbitrary set of extra properties, define a POJO with a Map for the dynamic part and annotate a setter with @JsonAnySetter:
public class User {
private String name;
private String email;
private Map<String, Object> extra = new HashMap<>();
@JsonAnySetter
public void setExtra(String key, Object value) {
extra.put(key, value);
}
}
@PostMapping("/user")
public ResponseEntity<String> createUser(@RequestBody User user) {
return ResponseEntity.ok(user);
}Combines strong‑type safety for known fields with flexibility for unknown ones.
Jackson automatically captures unknown properties.
Drawback: Model becomes slightly more complex.
Applicable when APIs evolve frequently and clients may add new fields.
2.5 Jackson Polymorphic Deserialization
If the dynamic JSON belongs to a limited set of known structures (e.g., different event types), use Jackson’s polymorphic features with @JsonTypeInfo and @JsonSubTypes:
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = TextMessageRequest.class, name = "text"),
@JsonSubTypes.Type(value = ImageMessageRequest.class, name = "image")
})
public abstract class BaseMessageRequest {
private String type;
private String fromUser;
}
public class TextMessageRequest extends BaseMessageRequest {
private String content;
}
public class ImageMessageRequest extends BaseMessageRequest {
private String imageUrl;
private int size;
}
@PostMapping
public ResponseEntity<String> handleMessage(@RequestBody BaseMessageRequest request) {
if (request instanceof TextMessageRequest textMsg) {
return ResponseEntity.ok("Content: %s".formatted(textMsg.getContent()));
} else if (request instanceof ImageMessageRequest imgMsg) {
return ResponseEntity.ok("Image URL: %s, Size: %s".formatted(imgMsg.getImageUrl(), imgMsg.getSize()));
}
return ResponseEntity.badRequest().body("Unknown type");
}Strong typing for each variant while keeping a single endpoint.
Jackson automatically selects the correct subclass based on the type field.
Drawback: Requires additional class definitions.
Fit for APIs where the payload type is indicated by a discriminator field.
2.6 Validate Dynamic JSON
After receiving a dynamic structure (e.g., Map or JsonNode), you often need to ensure it complies with business rules. The networknt/json-schema-validator library can validate JSON at runtime against a JSON‑Schema definition.
Dependency (Maven) :
<dependency>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>
<version>2.0.1</version>
</dependency>Note: Use the matching 3.x version of the library if your Jackson version is 3.x.
Example validation controller:
@RestController
public class SchemaValidationController {
private final Schema schema;
public SchemaValidationController() {
SchemaRegistryConfig config = SchemaRegistryConfig.builder()
.regularExpressionFactory(JoniRegularExpressionFactory.getInstance())
.build();
SchemaRegistry registry = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12,
builder -> builder.schemaRegistryConfig(config)
.schemaIdResolvers(resolvers -> resolvers.mapPrefix("http://www.pack.com/schema", "classpath:schema")));
String schemaJson = """
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"age": { "type": "number", "minimum": 0 },
"name": { "type": "string" }
},
"required": ["name"]
}
""";
schema = registry.getSchema(schemaJson);
}
@PostMapping("/validate-dynamic")
public ResponseEntity<?> handleDynamicWithSchema(@RequestBody JsonNode payload) {
List<Error> errors = schema.validate(payload);
if (errors.isEmpty()) {
return ResponseEntity.ok("success");
}
List<String> result = errors.stream()
.map(err -> err.getInstanceLocation() + ":" + err.getMessage())
.toList();
return ResponseEntity.ok(result);
}
}The controller loads a JSON schema, validates incoming payloads, and returns either a success message or a list of validation errors.
Conclusion
The article presents five concrete techniques for handling dynamic JSON in Spring Boot, each with clear code samples, pros and cons, and appropriate usage scenarios. It also demonstrates how to plug in runtime JSON‑Schema validation to keep dynamic payloads safe and compliant.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
