BMX RainTree is a lightweight Java library that enforces a standardized JSON response envelope β the RainTree protocol β for RESTful APIs built in Spring Boot. Every response produced by this library carries a guaranteed success flag and message string at the JSON root, with optional typed payload fields. The library provides three tiers of response construction, from simple one-liners to fully typed abstract response builders.
- Group ID:
com.bloomscorp - Artifact ID:
bmx-raintree - Current Version:
0.0.14 - Java: 21+
- License: MIT
- Published to: Maven Central
- Why BMX RainTree?
- Installation
- Package Structure
- The RainTree Protocol
- API Reference
- Usage Guide
- Simple responses with RainTree
- Parameterized responses
- Handling CRUD outcomes with ActionCode
- Typed entity responses with RainEntity
- Typed failure responses with RainFailedEntity
- Abstract response builders with RainResponse
- Parameterized builders with RainEnhancedResponse
- Excluding fields from serialization
- Response Format Reference
- Design Notes
- Dependencies
- License
In a multi-service Spring Boot ecosystem, each controller tends to return its own ad-hoc JSON structure. One endpoint returns {"ok": true}, another returns {"status": "success"}, a third returns the raw entity with no envelope at all. Consumers cannot write generic error handling β every integration needs its own parsing logic.
BMX RainTree solves this by providing:
- A fixed protocol contract β every response, regardless of endpoint or service, always contains
"success"and"message"at the root. Consumers can always check these two fields before inspecting any payload. - Three tiers of response construction β flat JSON strings for simple cases, typed POJO wrappers for entity-carrying responses, and abstract builder classes for domain-specific response classes.
- Gson-based serialization with field exclusion β a shared, pre-configured Gson instance handles all serialization, and the
@GsonExcludeannotation lets specific fields be silently omitted from output without changing the domain model. - Seamless ActionCode integration β pairs with bmx-hastar so CRUD operation outcomes map directly to typed, pre-worded responses without string literals scattered across the codebase.
<dependency>
<groupId>com.bloomscorp</groupId>
<artifactId>bmx-raintree</artifactId>
<version>0.0.14</version>
</dependency>implementation("com.bloomscorp:bmx-raintree:0.0.14")implementation 'com.bloomscorp:bmx-raintree:0.0.14'com.bloomscorp.raintree
β
βββ RainTree.java # Core response engine β produces all JSON strings
βββ RainTreeResponse.java # Base response POJO with success + message
β
βββ configuration/
β βββ GsonExclude.java # Field-level annotation to exclude from serialization
β βββ GsonExclusionStrategy.java # Gson ExclusionStrategy that enforces @GsonExclude
β
βββ restful/
β βββ RainEntity.java # Typed response envelope for a single entity payload
β βββ RainFailedEntity.java # Typed failure envelope β always success=false, entity=null
β βββ RainResponse.java # Abstract base for domain-specific response builders
β βββ RainEnhancedResponse.java # Abstract extension for parameterized response builders
β
βββ support/
βββ Constant.java # String constants for JSON keys and default messages
Every JSON response produced or returned by this library adheres to the following contract:
- Always has
"success"β abooleanat the JSON root.truefor success,falsefor failure. - Always has
"message"β aStringat the JSON root. Human-readable outcome description. - Optionally has payload fields β either merged at the root as named keys, or in a typed
"entity"field, depending on the response format used.
The canonical response shapes are:
Shape 1 β Simple:
{
"success": true,
"message": "The request was successful."
}Shape 2 β Parameterized (list under a named key):
{
"success": true,
"message": "",
"users": [
{ "id": 1, "name": "Alice" },
{ "id": 2, "name": "Bob" }
]
}Shape 3 β Parameterized (single entity under a named key):
{
"success": true,
"message": "",
"user": { "id": 1, "name": "Alice" }
}Shape 4 β Typed entity (via RainEntity serialization):
{
"success": true,
"message": null,
"entity": { "id": 1, "name": "Alice" }
}Shape 5 β Typed failure (via RainFailedEntity serialization):
{
"success": false,
"message": "The username does not have any associated account.",
"entity": null
}Package: com.bloomscorp.raintree
The core engine. Holds a private static final thread-safe Gson instance configured with GsonExclusionStrategy. All JSON production in the library ultimately flows through one of its methods. Safe to register as a singleton Spring bean.
Methods:
| Method | Return | Description |
|---|---|---|
renderResponse(Object object) |
String |
Serializes any Java object to JSON. No envelope is injected. |
renderResponse(boolean success, String message) |
String |
Produces {"success": ..., "message": "..."}. |
renderResponse(boolean success, String message, HashMap<String, Object> parameters) |
String |
Standard envelope with additional fields merged at the JSON root. parameters may be null. |
successResponse() |
String |
{"success": true, "message": "The request was successful."} |
successResponse(String message) |
String |
{"success": true, "message": "..."} |
failureResponse() |
String |
{"success": false, "message": "The request failed."} |
failureResponse(String message) |
String |
{"success": false, "message": "..."} |
failureResponse(int actionCode) |
String |
{"success": false, "message": "<ActionCode resolved message>"} |
renderParameterizedSuccessResponse(HashMap<String, Object> parameters) |
String |
{"success": true, "message": "", ...parameters} |
Package: com.bloomscorp.raintree
The base POJO that represents the protocol's minimum response shape. Used as a return type wherever a simple success/failure object is needed β for example, from RainResponse.prepareActionResponse.
Fields:
| Field | Type | Description |
|---|---|---|
success |
boolean |
true if the operation succeeded, false otherwise. |
message |
String |
Human-readable outcome description. |
Constructors: No-args (@NoArgsConstructor) and all-args (@AllArgsConstructor) generated by Lombok.
Package: com.bloomscorp.raintree.restful
Extends: RainTreeResponse
A typed envelope that carries a single domain entity. The success field is automatically derived from the entity's nullability β a non-null entity means success, a null entity means failure.
Fields:
| Field | Type | Description |
|---|---|---|
success |
boolean |
(inherited) Auto-set to entity != null by the single-arg constructor. |
message |
String |
(inherited) Not populated by this class; defaults to null. |
entity |
E |
The domain entity payload. null if the operation found nothing. |
Constructors:
| Constructor | Behavior |
|---|---|
RainEntity() |
No-args. All fields remain at their Java defaults. |
RainEntity(E entity) |
Sets this.entity = entity and this.success = entity != null. |
Package: com.bloomscorp.raintree.restful
Extends: RainEntity<E>
A fixed-failure envelope. success and entity are hardwired to false and null via field initializers. Use this when you need to return a typed RainEntity-shaped response that unambiguously signals failure, including the reason why.
Fields:
| Field | Type | Value | Description |
|---|---|---|---|
success |
boolean |
Always false |
Shadows the parent's success field via initializer. |
entity |
E |
Always null |
Shadows the parent's entity field via initializer. No entity is available. |
message |
String |
Set by constructor | Human-readable failure explanation. Typically sourced from com.bloomscorp.hastar.support.Message. |
Constructor:
new RainFailedEntity<>(String message)Package: com.bloomscorp.raintree.restful
Abstract base class for domain-specific response builders. Subclasses implement buildEntity and buildList to define the JSON key names under which payloads are nested. Holds a RainTree instance injected via the constructor.
Constructor:
// Typically called as super(rainTree) from a subclass
new RainResponse<E>(RainTree rainTree)Methods:
| Method | Modifier | Return | Description |
|---|---|---|---|
prepareActionResponse(int action) |
public static |
RainTreeResponse |
Maps an ActionCode integer to a RainTreeResponse with the correct success and message. |
prepareList(List<? extends T> list, String responseParameter) |
protected |
String |
Produces a parameterized success JSON with the list nested under responseParameter. |
prepareEntity(T object, String responseParameter) |
protected |
String |
Produces a parameterized success JSON with the object nested under responseParameter. |
buildEntity(E entity) |
public abstract |
String |
Subclasses define the entity key name and delegate to prepareEntity. |
buildList(List<E> list) |
public abstract |
String |
Subclasses define the list key name and delegate to prepareList. |
prepareActionResponse success mapping:
| ActionCode constant | Value | success result |
|---|---|---|
INSERT_SUCCESS |
1 |
true |
UPDATE_SUCCESS |
2 |
true |
DELETE_SUCCESS |
3 |
true |
| Any other code | β | false |
Package: com.bloomscorp.raintree.restful
Extends: RainResponse<E>
Extends RainResponse with a third abstract method for cases where building a response requires a runtime parameter of type P beyond the entity type E alone β for example, a filter criterion, a request-time identifier, or a context object.
Constructor:
new RainEnhancedResponse<E, P>(RainTree rainTree)Methods:
| Method | Modifier | Return | Description |
|---|---|---|---|
prepareResponse(P parameter) |
public abstract |
RainTreeResponse |
Subclasses implement this for response logic that requires a runtime parameter not expressible through buildEntity or buildList. |
(all of RainResponse<E>) |
All inherited methods and abstract methods from the parent are also present. |
Package: com.bloomscorp.raintree.configuration
Target: ElementType.FIELD
Retention: RetentionPolicy.RUNTIME
A marker annotation. Any field annotated with @GsonExclude is silently omitted from all JSON output produced by RainTree's Gson instance. Has no effect on Jackson-based serialization.
Package: com.bloomscorp.raintree.configuration
Implements: com.google.gson.ExclusionStrategy
Implements Gson's ExclusionStrategy to skip fields annotated with @GsonExclude. Registered on the static Gson instance inside RainTree. Class-level exclusion is never applied β filtering is strictly field-level.
Package: com.bloomscorp.raintree.support
Non-instantiable utility class holding all string constants used by the library internally.
| Constant | Value |
|---|---|
BLANK_STRING_VALUE |
"" |
JSON_RESPONSE_SUCCESS |
"success" |
JSON_RESPONSE_MESSAGE |
"message" |
JSON_RESPONSE_GENERIC_SUCCESS |
"The request was successful." |
JSON_RESPONSE_GENERIC_FAILURE |
"The request failed." |
Register RainTree as a Spring bean and inject it wherever a controller or service needs to return a response string.
@Configuration
public class RainTreeConfig {
@Bean
public RainTree rainTree() {
return new RainTree();
}
}@RestController
@RequestMapping("/api/ping")
public class PingController {
private final RainTree rainTree;
public PingController(RainTree rainTree) {
this.rainTree = rainTree;
}
@GetMapping
public String ping() {
return this.rainTree.successResponse();
// β {"success":true,"message":"The request was successful."}
}
@GetMapping("/fail")
public String forceFail() {
return this.rainTree.failureResponse("Service is temporarily unavailable.");
// β {"success":false,"message":"Service is temporarily unavailable."}
}
}Use renderParameterizedSuccessResponse when the response needs to carry additional fields alongside the standard envelope, without defining a full typed class:
@GetMapping("/stats")
public String getStats() {
HashMap<String, Object> params = new HashMap<>();
params.put("totalUsers", userRepository.count());
params.put("activeUsers", userRepository.countByActive(true));
return this.rainTree.renderParameterizedSuccessResponse(params);
// β {"success":true,"message":"","totalUsers":1042,"activeUsers":891}
}Pair RainTree with bmx-hastar's ActionCode to translate integer service-layer outcomes into well-worded responses without hardcoded strings:
// In a service class
public int createUser(UserDto dto) {
try {
this.userRepository.save(toEntity(dto));
return ActionCode.INSERT_SUCCESS;
} catch (DataIntegrityViolationException e) {
return ActionCode.NOT_UNIQUE;
} catch (Exception e) {
return ActionCode.INSERT_FAILURE;
}
}// In a controller β string-based approach
@PostMapping("/users")
public String createUser(@RequestBody UserDto dto) {
int result = this.userService.createUser(dto);
if (result == ActionCode.INSERT_SUCCESS)
return this.rainTree.successResponse(ActionCode.message(result));
return this.rainTree.failureResponse(result);
// INSERT_FAILURE β {"success":false,"message":"The request failed to add to database."}
// NOT_UNIQUE β {"success":false,"message":"The request is not unique."}
}// In a controller β POJO-based approach using prepareActionResponse
@PostMapping("/users")
public RainTreeResponse createUser(@RequestBody UserDto dto) {
int result = this.userService.createUser(dto);
return RainResponse.prepareActionResponse(result);
// INSERT_SUCCESS β {"success":true,"message":"The request is successfully added to database."}
// INSERT_FAILURE β {"success":false,"message":"The request failed to add to database."}
}Use RainEntity<E> when a controller returns a single domain object and success should be derived automatically from whether the entity exists:
@GetMapping("/users/{id}")
public RainEntity<User> getUser(@PathVariable Long id) {
User user = this.userRepository.findById(id).orElse(null);
return new RainEntity<>(user);
// found β {"success":true,"message":null,"entity":{"id":1,"name":"Alice"}}
// not found β {"success":false,"message":null,"entity":null}
}If a String response is needed instead of a POJO, serialize through RainTree:
@GetMapping("/users/{id}")
public String getUser(@PathVariable Long id) {
User user = this.userRepository.findById(id).orElse(null);
return this.rainTree.renderResponse(new RainEntity<>(user));
}Use RainFailedEntity<E> when the caller always expects a RainEntity-shaped envelope but the operation failed for a specific, communicable reason:
@GetMapping("/users/{id}")
public RainEntity<User> getUser(@PathVariable Long id, Principal principal) {
if (!this.authService.canViewUser(principal, id))
return new RainFailedEntity<>(Message.AUTHORIZATION_DENIED);
// β {"success":false,"message":"Authorization has been denied.","entity":null}
User user = this.userRepository.findById(id).orElse(null);
if (user == null)
return new RainFailedEntity<>(Message.USERNAME_NOT_FOUND);
// β {"success":false,"message":"The username does not have any associated account.","entity":null}
return new RainEntity<>(user);
// β {"success":true,"message":null,"entity":{"id":1,"name":"Alice"}}
}For a domain resource that frequently returns lists or entities under a consistent key name, extend RainResponse<E> once and reuse across all controllers that work with that resource:
@Component
public class UserRainResponse extends RainResponse<User> {
public UserRainResponse(RainTree rainTree) {
super(rainTree);
}
@Override
public String buildEntity(User user) {
return this.prepareEntity(user, "user");
// β {"success":true,"message":"","user":{"id":1,"name":"Alice"}}
}
@Override
public String buildList(List<User> users) {
return this.prepareList(users, "users");
// β {"success":true,"message":"","users":[{"id":1,"name":"Alice"},{"id":2,"name":"Bob"}]}
}
}@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
private final UserRainResponse userRainResponse;
public UserController(UserService userService, UserRainResponse userRainResponse) {
this.userService = userService;
this.userRainResponse = userRainResponse;
}
@GetMapping
public String getAllUsers() {
return this.userRainResponse.buildList(this.userService.findAll());
}
@GetMapping("/{id}")
public String getUser(@PathVariable Long id) {
return this.userRainResponse.buildEntity(this.userService.findById(id));
}
@PostMapping
public RainTreeResponse createUser(@RequestBody UserDto dto) {
int result = this.userService.create(dto);
return UserRainResponse.prepareActionResponse(result);
}
@PutMapping("/{id}")
public RainTreeResponse updateUser(@PathVariable Long id, @RequestBody UserDto dto) {
int result = this.userService.update(id, dto);
return UserRainResponse.prepareActionResponse(result);
}
@DeleteMapping("/{id}")
public RainTreeResponse deleteUser(@PathVariable Long id) {
int result = this.userService.delete(id);
return UserRainResponse.prepareActionResponse(result);
}
}Extend RainEnhancedResponse<E, P> when response construction requires a runtime parameter that is not the entity itself β such as a search term, a filter object, or a configuration value:
@Component
public class UserSearchRainResponse extends RainEnhancedResponse<User, String> {
private final UserService userService;
public UserSearchRainResponse(RainTree rainTree, UserService userService) {
super(rainTree);
this.userService = userService;
}
@Override
public String buildEntity(User user) {
return this.prepareEntity(user, "user");
}
@Override
public String buildList(List<User> users) {
return this.prepareList(users, "users");
}
@Override
public RainTreeResponse prepareResponse(String searchTerm) {
List<User> results = this.userService.search(searchTerm);
boolean found = !results.isEmpty();
return new RainTreeResponse(
found,
found ? Message.INSERT_SUCCESS : Message.INCOMPLETE_INFORMATION
);
}
}@GetMapping("/search")
public RainTreeResponse search(@RequestParam String q) {
return this.userSearchRainResponse.prepareResponse(q);
}Annotate any field with @GsonExclude to prevent it from appearing in JSON output produced by RainTree:
public class User {
public Long id;
public String name;
public String email;
@GsonExclude
public String passwordHash;
@GsonExclude
public String internalAuditToken;
}this.rainTree.renderResponse(new RainEntity<>(user));
// β {"success":true,"message":null,"entity":{"id":1,"name":"Alice","email":"alice@example.com"}}
// passwordHash and internalAuditToken are absent from the outputNote:
@GsonExcludeonly applies when serialization goes throughRainTree's Gson instance β i.e., when you callrainTree.renderResponse(object)or return a JSON string from theRainTreemethods. If aRainEntityorRainFailedEntityis returned directly as a@RestControllerresponse body without going throughrenderResponse, Spring will serialize it using Jackson. In that path,@GsonExcludehas no effect β use@JsonIgnorefor Jackson-based exclusion.
| Source | success |
message |
Payload shape |
|---|---|---|---|
successResponse() |
true |
"The request was successful." |
none |
successResponse(msg) |
true |
custom | none |
failureResponse() |
false |
"The request failed." |
none |
failureResponse(msg) |
false |
custom | none |
failureResponse(actionCode) |
false |
resolved from ActionCode |
none |
renderResponse(success, message) |
custom | custom | none |
renderParameterizedSuccessResponse(params) |
true |
"" |
params merged at root |
new RainEntity<>(entity) β entity non-null |
true |
null |
"entity": <E> |
new RainEntity<>(null) |
false |
null |
"entity": null |
new RainFailedEntity<>(msg) |
false |
custom | "entity": null |
prepareActionResponse(INSERT/UPDATE/DELETE_SUCCESS) |
true |
resolved from ActionCode |
none |
prepareActionResponse(any other code) |
false |
resolved from ActionCode |
none |
buildEntity(entity) |
true |
"" |
"<key>": <E> at root |
buildList(list) |
true |
"" |
"<key>": [<E>] at root |
Gson, not Jackson. The library uses Gson for all serialization rather than Spring's default Jackson. This gives the library full control over serialization behavior regardless of how the consuming application configures its ObjectMapper. The trade-off is that @JsonIgnore and other Jackson annotations have no effect inside RainTree; use @GsonExclude instead.
@GsonExclude only covers RainTree output. When a RainEntity or RainFailedEntity is returned directly as a @RestController response body β without going through renderResponse β Spring uses Jackson to serialize it. @GsonExclude does not apply in that path.
RainFailedEntity uses field hiding, not constructor logic. The success = false and entity = null values in RainFailedEntity are set as field initializers on the class itself, not inside the constructor. They shadow the same-named fields from RainTreeResponse and RainEntity. Gson serializes based on the declared fields of the runtime class, so the overrides take effect correctly in JSON output.
RainEntity.message is intentionally null by default. The single-arg constructor of RainEntity only sets entity and derives success. The message field inherited from RainTreeResponse is left at its Java default (null). Entity-carrying responses are not expected to carry a message β consumers should check success and entity, not message, when working with RainEntity or RainFailedEntity directly.
Thread safety. The Gson instance inside RainTree is private static final and constructed once at class load time. Gson instances are thread-safe for concurrent read operations, so RainTree is safe to register as a singleton Spring bean without any synchronization.
No Spring dependency in the library itself. The library has no compile-time dependency on spring-boot, spring-web, or spring-security. It is pure Java 21 and can be included in any Java project without risk of framework version conflicts.
ActionCode sign convention. ActionCode values from bmx-hastar follow a deliberate sign convention: > 0 is success, 0 is no-op, < 0 is failure. RainResponse.prepareActionResponse only maps the three positive success codes to success = true; everything else β including 0 and all negative codes β maps to success = false.
| Dependency | Version | Scope |
|---|---|---|
com.google.code.gson:gson |
2.11.0 |
Compile |
org.projectlombok:lombok |
1.18.36 |
Optional (annotation processor) |
org.jetbrains:annotations |
26.0.1 |
Compile |
com.bloomscorp:bmx-hastar |
0.0.3 |
Compile |
BMX RainTree is released under the MIT License.
Copyright Β© 2023 Bloomscorp