Skip to content

Commit 06be21a

Browse files
authored
feat(validation): align request/response validation and harden parsing/metrics behavior (#21)
## Summary This PR completes the validation/options overhaul for JSON-RPC request/response handling and aligns Spring auto-configuration with core behavior. Key updates: - Added symmetric request/response validation options in `jsonrpc-core` (ID rules, duplicate-member policies, field rejection policies). - Added strict duplicate-member parsing support via shared payload reader and wired it through core/webmvc/autoconfigure. - Exposed and bound response parser/validator options in Spring Boot auto-configuration. - Fixed autoconfigure precedence so custom `JsonRpcRequestValidationOptions` / `JsonRpcResponseValidationOptions` beans are honored consistently. - Added hard fail-fast constraints for invalid constructor/property inputs (including positive-only metric method-tag cap). - Enforced hard cap semantics for metric method-tag cardinality under concurrency. - Expanded tests across core/autoconfigure/webmvc/samples for success/failure/branch/concurrency paths. - Updated docs and metadata (configuration reference, migration mapping, sample guides, expected JSON outputs). ## Related Issues - Closes #20 - Related # ## Change Type - [x] Bug fix - [x] Feature - [x] Refactor - [x] Documentation - [x] Test - [x] Build/CI ## JSON-RPC Impact Describe protocol-level impact, if any: - [ ] No protocol behavior change - [x] Request validation behavior changed - [ ] Error mapping behavior changed - [ ] Method registration/dispatch behavior changed Details: - Validation behavior is now more explicitly controlled through symmetric request/response options. - Duplicate-member rejection can be enforced consistently for request/response raw parsing paths. - Reserved method namespace (`rpc.*`) rejection and stricter fail-fast constraints are enforced where configured. - Default behavior remains RFC-oriented and documented; strict toggles are opt-in via configuration. ## Validation - [x] `./gradlew test` - [x] `./gradlew check` - [x] Added/updated tests for new behavior ## Documentation - [x] Updated README (if needed) - [x] Added migration notes for breaking changes (if any)
1 parent d65fb61 commit 06be21a

45 files changed

Lines changed: 3775 additions & 288 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,11 +183,16 @@ Use `jsonrpc.validation.request.*` and `jsonrpc.validation.response.*` for fine-
183183
jsonrpc:
184184
validation:
185185
request:
186+
require-id-member: false
187+
allow-fractional-id: false
188+
reject-response-fields: true
186189
params-type-violation-code-policy: INVALID_REQUEST
187190
response:
188-
require-response-id-member: true
189-
allow-fractional-response-id: false
190-
allow-request-fields-in-response: false
191+
require-id-member: true
192+
allow-fractional-id: false
193+
reject-request-fields: true
194+
error-code:
195+
policy: STANDARD_OR_SERVER_ERROR_RANGE
191196
```
192197
193198
For the full list of validation keys and defaults, see

docs/configuration-reference.md

Lines changed: 90 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,30 @@ All properties are under `jsonrpc.*` and are bound to `JsonRpcProperties`.
1212
| `jsonrpc.max-request-bytes` | `int` | `1048576` | Raw HTTP request payload size limit in bytes |
1313
| `jsonrpc.scan-annotated-methods` | `boolean` | `true` | Scan Spring beans for `@JsonRpcMethod` |
1414
| `jsonrpc.include-error-data` | `boolean` | `false` | Include `JsonRpcException.data` in error responses |
15+
| `jsonrpc.validation.request.require-json-rpc-version-20` | `boolean` | `true` | Require incoming request `jsonrpc` to equal `"2.0"` |
16+
| `jsonrpc.validation.request.require-id-member` | `boolean` | `false` | Require incoming requests to include an `id` member |
17+
| `jsonrpc.validation.request.allow-null-id` | `boolean` | `true` | Allow `id: null` in incoming requests |
18+
| `jsonrpc.validation.request.allow-string-id` | `boolean` | `true` | Allow string IDs in incoming requests |
19+
| `jsonrpc.validation.request.allow-numeric-id` | `boolean` | `true` | Allow numeric IDs in incoming requests |
20+
| `jsonrpc.validation.request.allow-fractional-id` | `boolean` | `true` | Allow fractional numeric IDs in incoming requests |
21+
| `jsonrpc.validation.request.reject-response-fields` | `boolean` | `false` | Reject request objects containing `result`/`error` |
22+
| `jsonrpc.validation.request.reject-duplicate-members` | `boolean` | `false` | Reject duplicate members while parsing raw request JSON |
1523
| `jsonrpc.validation.request.params-type-violation-code-policy` | `INVALID_PARAMS` or `INVALID_REQUEST` | `INVALID_PARAMS` | Error code used when `params` exists but is neither object nor array |
1624
| `jsonrpc.validation.response.require-json-rpc-version-20` | `boolean` | `true` | Require incoming response `jsonrpc` to equal `"2.0"` |
17-
| `jsonrpc.validation.response.require-response-id-member` | `boolean` | `true` | Require incoming responses to include an `id` member |
18-
| `jsonrpc.validation.response.allow-null-response-id` | `boolean` | `true` | Allow `id: null` in incoming responses |
19-
| `jsonrpc.validation.response.allow-string-response-id` | `boolean` | `true` | Allow string IDs in incoming responses |
20-
| `jsonrpc.validation.response.allow-numeric-response-id` | `boolean` | `true` | Allow numeric IDs in incoming responses |
21-
| `jsonrpc.validation.response.allow-fractional-response-id` | `boolean` | `true` | Allow fractional numeric IDs in incoming responses |
25+
| `jsonrpc.validation.response.require-id-member` | `boolean` | `true` | Require incoming responses to include an `id` member |
26+
| `jsonrpc.validation.response.allow-null-id` | `boolean` | `true` | Allow `id: null` in incoming responses |
27+
| `jsonrpc.validation.response.allow-string-id` | `boolean` | `true` | Allow string IDs in incoming responses |
28+
| `jsonrpc.validation.response.allow-numeric-id` | `boolean` | `true` | Allow numeric IDs in incoming responses |
29+
| `jsonrpc.validation.response.allow-fractional-id` | `boolean` | `true` | Allow fractional numeric IDs in incoming responses |
2230
| `jsonrpc.validation.response.require-exclusive-result-or-error` | `boolean` | `true` | Require exactly one of `result` or `error` |
2331
| `jsonrpc.validation.response.require-error-object-when-present` | `boolean` | `true` | Require `error` to be an object when present |
2432
| `jsonrpc.validation.response.require-integer-error-code` | `boolean` | `true` | Require `error.code` to be an integer |
2533
| `jsonrpc.validation.response.require-string-error-message` | `boolean` | `true` | Require `error.message` to be a string |
26-
| `jsonrpc.validation.response.allow-request-fields-in-response` | `boolean` | `true` | Allow request-only fields (`method`/`params`) on responses |
34+
| `jsonrpc.validation.response.reject-request-fields` | `boolean` | `false` | Reject response objects containing `method`/`params` |
35+
| `jsonrpc.validation.response.reject-duplicate-members` | `boolean` | `false` | Reject duplicate members while parsing raw response JSON |
36+
| `jsonrpc.validation.response.error-code.policy` | `JsonRpcResponseErrorCodePolicy` | `ANY_INTEGER` | Accepted integer range policy for response `error.code` |
37+
| `jsonrpc.validation.response.error-code.range.min` | `Integer` | `null` | Inclusive minimum for `CUSTOM_RANGE` |
38+
| `jsonrpc.validation.response.error-code.range.max` | `Integer` | `null` | Inclusive maximum for `CUSTOM_RANGE` |
2739
| `jsonrpc.method-registration-conflict-policy` | `REJECT` or `REPLACE` | `REJECT` | Duplicate method name registration policy |
2840
| `jsonrpc.method-allowlist` | `List<String>` | `[]` | Allowlist for method access filtering |
2941
| `jsonrpc.method-denylist` | `List<String>` | `[]` | Denylist for method access filtering (higher priority) |
@@ -34,6 +46,12 @@ All properties are under `jsonrpc.*` and are bound to `JsonRpcProperties`.
3446
| `jsonrpc.notification-executor-enabled` | `boolean` | `false` | Enable executor-backed notification dispatch |
3547
| `jsonrpc.notification-executor-bean-name` | `String` | `""` | Preferred executor bean name for notifications |
3648

49+
`JsonRpcResponseErrorCodePolicy` values:
50+
- `ANY_INTEGER`
51+
- `STANDARD_ONLY`
52+
- `STANDARD_OR_SERVER_ERROR_RANGE`
53+
- `CUSTOM_RANGE`
54+
3755
## 2. Validation Rules (Fail Fast)
3856

3957
Startup fails with `IllegalArgumentException` when any of these conditions occur:
@@ -52,12 +70,18 @@ Startup fails with `IllegalArgumentException` when any of these conditions occur
5270
- `jsonrpc.validation.request` is null
5371
- `jsonrpc.validation.request.params-type-violation-code-policy` is null
5472
- `jsonrpc.validation.response` is null
73+
- `jsonrpc.validation.response.error-code` is null
74+
- `jsonrpc.validation.response.error-code.policy` is null
75+
- `jsonrpc.validation.response.error-code.range` is null
76+
- `jsonrpc.validation.response.require-integer-error-code=false` with `jsonrpc.validation.response.error-code.policy != ANY_INTEGER`
77+
- `jsonrpc.validation.response.error-code.policy=CUSTOM_RANGE` and either range bound is missing
78+
- `jsonrpc.validation.response.error-code.policy=CUSTOM_RANGE` and `range.min > range.max`
5579
- allowlist/denylist list itself is null
5680
- allowlist/denylist contains null or blank values
5781

5882
## 3. Runtime Behavior Priority
5983

60-
## 3.1 Method access filtering
84+
### 3.1 Method access filtering
6185

6286
Priority:
6387

@@ -69,9 +93,10 @@ Rules:
6993

7094
- denylist always overrides allowlist
7195
- if allowlist is non-empty, methods not in allowlist are denied
72-
- `rpc.*` methods are blocked by registry independently of allow/deny lists
96+
- `rpc.*` methods are blocked by request validation, and also by the default registry independently of allow/deny
97+
lists
7398

74-
## 3.2 Notification executor resolution
99+
### 3.2 Notification executor resolution
75100

76101
When `jsonrpc.notification-executor-enabled=true`, resolution order is:
77102

@@ -82,7 +107,7 @@ When `jsonrpc.notification-executor-enabled=true`, resolution order is:
82107

83108
If a configured bean name is missing, startup fails.
84109

85-
## 3.3 Method registration conflict handling
110+
### 3.3 Method registration conflict handling
86111

87112
- `REJECT`: first duplicate fails registration.
88113
- `REPLACE`: later registration wins.
@@ -108,7 +133,7 @@ Example environment variable mapping:
108133

109134
## 5. Example Configurations
110135

111-
## 5.1 Baseline production profile
136+
### 5.1 Baseline production profile
112137

113138
```yaml
114139
jsonrpc:
@@ -120,40 +145,57 @@ jsonrpc:
120145
include-error-data: false
121146
validation:
122147
request:
148+
require-json-rpc-version-20: true
149+
require-id-member: false
150+
allow-null-id: true
151+
allow-string-id: true
152+
allow-numeric-id: true
153+
allow-fractional-id: true
154+
reject-response-fields: false
155+
reject-duplicate-members: false
123156
params-type-violation-code-policy: INVALID_PARAMS
124157
response:
125158
require-json-rpc-version-20: true
126-
require-response-id-member: true
127-
allow-null-response-id: true
128-
allow-string-response-id: true
129-
allow-numeric-response-id: true
130-
allow-fractional-response-id: true
159+
require-id-member: true
160+
allow-null-id: true
161+
allow-string-id: true
162+
allow-numeric-id: true
163+
allow-fractional-id: true
131164
require-exclusive-result-or-error: true
132165
require-error-object-when-present: true
133166
require-integer-error-code: true
134167
require-string-error-message: true
135-
allow-request-fields-in-response: true
168+
reject-request-fields: false
169+
reject-duplicate-members: false
170+
error-code:
171+
policy: ANY_INTEGER
172+
range:
173+
min: null
174+
max: null
136175
method-allowlist: [ ]
137176
method-denylist: [ ]
138177
```
139178
140-
## 5.2 Strict access profile
179+
### 5.2 Strict response error-code profile
141180
142181
```yaml
143182
jsonrpc:
144-
method-allowlist: [ user.find, user.update ]
145-
method-denylist: [ user.delete ]
183+
validation:
184+
response:
185+
reject-request-fields: true
186+
error-code:
187+
policy: STANDARD_OR_SERVER_ERROR_RANGE
146188
```
147189
148-
## 5.3 Async notification profile
190+
### 5.3 Async notification profile
149191
150192
```yaml
151193
jsonrpc:
152194
notification-executor-enabled: true
153195
notification-executor-bean-name: applicationTaskExecutor
154196
```
155197
156-
## 5.4 Metrics-rich profile
198+
### 5.4 Metrics-rich profile
157199
158200
```yaml
159201
jsonrpc:
@@ -163,7 +205,26 @@ jsonrpc:
163205
metrics-max-method-tag-values: 200
164206
```
165207
166-
## 6. IDE Auto-completion and Metadata
208+
## 6. Migration Notes (Response Validation Key Rename)
209+
210+
The old keys below are migration references only.
211+
They are not bound by current auto-configuration and should not be used in new setups.
212+
Use the canonical symmetric keys in the right column.
213+
214+
| Old key | New key |
215+
|----------------------------------------------------------------|-----------------------------------------------------|
216+
| `jsonrpc.validation.response.require-response-id-member` | `jsonrpc.validation.response.require-id-member` |
217+
| `jsonrpc.validation.response.allow-null-response-id` | `jsonrpc.validation.response.allow-null-id` |
218+
| `jsonrpc.validation.response.allow-string-response-id` | `jsonrpc.validation.response.allow-string-id` |
219+
| `jsonrpc.validation.response.allow-numeric-response-id` | `jsonrpc.validation.response.allow-numeric-id` |
220+
| `jsonrpc.validation.response.allow-fractional-response-id` | `jsonrpc.validation.response.allow-fractional-id` |
221+
| `jsonrpc.validation.response.allow-request-fields-in-response` | `jsonrpc.validation.response.reject-request-fields` |
222+
223+
Inversion rule:
224+
225+
- `reject-request-fields = !allow-request-fields-in-response`
226+
227+
## 7. IDE Auto-completion and Metadata
167228

168229
The project ships Spring Boot configuration metadata via:
169230

@@ -173,10 +234,14 @@ The project ships Spring Boot configuration metadata via:
173234
This enables:
174235

175236
- property key completion
176-
- enum value suggestions (`REJECT`, `REPLACE`, `INVALID_PARAMS`, `INVALID_REQUEST`)
237+
- enum value suggestions (`REJECT`, `REPLACE`, `INVALID_PARAMS`, `INVALID_REQUEST`, error-code policy values)
238+
- example numeric suggestions for `jsonrpc.validation.response.error-code.range.min/max`
177239
- metadata hints in IntelliJ and Spring-aware tooling
178240

179-
## 7. Related References
241+
Hints are suggestions for IDE completion only. They do not restrict allowed runtime values unless validation rules
242+
explicitly enforce constraints.
243+
244+
## 8. Related References
180245

181246
- Spring setup details: [`spring-boot-guide.md`](spring-boot-guide.md)
182247
- Binding and registration details: [`registration-and-binding.md`](registration-and-binding.md)

docs/index.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,12 @@
2727
- Failure diagnosis and fixes: [`troubleshooting.md`](troubleshooting.md)
2828
- Release and publish flow: [`release-checklist.md`](release-checklist.md)
2929

30-
## 5. Repository-Level Docs
30+
## 5. Samples
31+
32+
- Spring Boot sample: [`../samples/spring-boot-demo/README.md`](../samples/spring-boot-demo/README.md)
33+
- Pure Java sample: [`../samples/pure-java-demo/README.md`](../samples/pure-java-demo/README.md)
34+
35+
## 6. Repository-Level Docs
3136

3237
- Project overview: [`../README.md`](../README.md)
3338
- Contribution workflow: [`../CONTRIBUTING.md`](../CONTRIBUTING.md)

docs/protocol-and-compliance.md

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ Implementation constants are in `JsonRpcErrorCode` and messages in `JsonRpcConst
5555
- `JsonRpcResponseValidator`
5656
- `JsonRpcResponseValidationOptions`
5757

58+
`DefaultJsonRpcResponseParser` can parse from `JsonNode`, `String`, or `byte[]`, and can optionally reject duplicate
59+
members during raw JSON parsing.
60+
5861
These APIs are transport-agnostic and useful for bidirectional channels (for example WebSocket) where
5962
request/response envelopes may arrive on the same connection.
6063

@@ -79,18 +82,21 @@ This library does not expose predefined strict/lenient modes; policy is controll
7982
`JsonRpcResponseValidationOptions` exposes per-rule switches:
8083

8184
- `requireJsonRpcVersion20` (default: `true`)
82-
- `requireResponseIdMember` (default: `true`)
83-
- `allowNullResponseId` (default: `true`)
84-
- `allowStringResponseId` (default: `true`)
85-
- `allowNumericResponseId` (default: `true`)
86-
- `allowFractionalResponseId` (default: `true`)
85+
- `requireIdMember` (default: `true`)
86+
- `allowNullId` (default: `true`)
87+
- `allowStringId` (default: `true`)
88+
- `allowNumericId` (default: `true`)
89+
- `allowFractionalId` (default: `true`)
8790
- `requireExclusiveResultOrError` (default: `true`)
8891
- `requireErrorObjectWhenPresent` (default: `true`)
8992
- `requireIntegerErrorCode` (default: `true`)
9093
- `requireStringErrorMessage` (default: `true`)
91-
- `allowRequestFieldsInResponse` (default: `true`)
94+
- `rejectRequestFields` (default: `false`)
95+
- `rejectDuplicateMembers` (default: `false`)
96+
- `errorCodePolicy` (default: `ANY_INTEGER`)
97+
- `errorCodeRangeMin` / `errorCodeRangeMax` (default: `null`, used with `CUSTOM_RANGE`)
9298

93-
`allowRequestFieldsInResponse=true` is a compatibility default and is not an RFC MUST rule.
99+
`rejectRequestFields=false` is a compatibility default and is not an RFC MUST rule.
94100

95101
## `id` Handling Details
96102

@@ -108,8 +114,9 @@ This library does not expose predefined strict/lenient modes; policy is controll
108114

109115
## Reserved Method Namespace
110116

111-
Methods starting with `rpc.` are rejected at registration (`IllegalArgumentException`) to preserve reserved namespace
112-
semantics.
117+
Methods starting with `rpc.` are treated as invalid requests during request validation (`-32600`).
118+
Default in-memory registration also rejects `rpc.*` method names (`IllegalArgumentException`), so both registration and
119+
dispatch paths preserve reserved namespace semantics.
113120

114121
## HTTP Mapping Notes
115122

docs/pure-java-guide.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ JsonRpcDispatcher dispatcher = new JsonRpcDispatcher(
207207
```
208208

209209
This keeps protocol behavior while letting you customize policy and implementation.
210+
`maxBatchSize` must be greater than `0` (fail-fast `IllegalArgumentException` otherwise).
210211

211212
`JsonRpcParamsTypeViolationCodePolicy` controls which code is used when request `params` is present but not
212213
an object/array:

0 commit comments

Comments
 (0)