feat(methods): add blocks.validate method - #1622
Conversation
Add the blocks.validate Web API method to the Slack API client. Callers can validate Block Kit payloads via methods().blocksValidate(req) (and the async equivalent), passing blocks, message, or view as JSON-encoded strings. No scopes are required. Adds the endpoint constant, sync and async interface methods and impls, the request form builder, the request/response model classes (with an errors[] list of code/message/pointer/relatedComponent), the Tier3 rate limit, a local test, and a response sample for type generation. Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #1622 +/- ##
============================================
- Coverage 72.87% 72.86% -0.02%
- Complexity 4522 4533 +11
============================================
Files 479 480 +1
Lines 14390 14418 +28
Branches 1503 1512 +9
============================================
+ Hits 10487 10505 +18
- Misses 3013 3018 +5
- Partials 890 895 +5
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. |
blocks.validate has special rate-limiting conditions rather than a standard tier (see https://docs.slack.dev/reference/methods/blocks.validate), so model it as SpecialTier_blocks_validate instead of Tier3. Regenerate the rate-limit metadata so the committed rate_limit_tiers.json matches the generated output, which also sorts blocks.validate into its correct alphabetical position — this is what was tripping the CI tree-drift check. Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
Add a blocks_Test integration test under test_with_remote_apis that exercises blocks.validate against the real API: a well-formed payload validates with no errors, and a malformed payload surfaces the live validation feedback (recording the actual contract — ok=false with an error, or ok=true with a populated errors[] carrying code/message/pointer). Refresh the method-coverage scrape marker in MethodsTest to reflect the current 310-endpoint list. Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
The method was consistently placed after the bookmarks family, but "blocks" sorts before "bookmarks". Move every insertion site into alphabetical order: request/response imports, the sync/async interface declarations and their implementations, the RequestFormBuilder toForm mapping, the rate-limit tier registration, and the method-coverage string in MethodsTest. Pure reordering — no behavior change. Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
blocks.validate requires no token or scopes (https://docs.slack.dev/reference/methods/blocks.validate), so it should not send an Authorization header. Model it like api.test: drop the token field from BlocksValidateRequest (overriding getToken() to return null) and call it through the tokenless postFormAndParseResponse path. The local BlocksTest now asserts the request round-trips (the shared mock answers a tokenless call with not_authed); end-to-end ok/errors[] behavior is covered by the remote blocks_Test. Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
The `blocks.validate` response `errors[]` entries carry pointer / code /
message / constraint per the method reference. The Error model previously
declared a `relatedComponent` field (copied from apps.manifest.validate),
which is not part of the blocks.validate contract, and omitted `constraint`
— the structured object describing what was expected
(e.g. {"type": "enum", "expected": ["plain_text", "mrkdwn"]}).
Replace `relatedComponent` with a `Constraint` nested type, fix the mock
sample to the documented shape, and clarify the remote-test comments and
assertions to reflect the real contract.
Ref: https://docs.slack.dev/reference/methods/blocks.validate#validation-errors
Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
Verified against the real (unauthenticated) blocks.validate endpoint:
- A section with an unsupported text "type" fails; a section with no
text/fields returns ok=true. Switch the malformed remote test to the
former so it deterministically exercises the errors[] path, and assert
the confirmed shape (ok=false, error="invalid_blocks").
- constraint carries a "got" field for enum failures (e.g.
{"type":"enum","expected":[...],"got":"invalid"}) and may be just
{"type":"all_of"} for composite-schema failures; add Constraint.got.
- Update the mock fixture to a real captured response, and trim the
redundant comments down to the field contract + doc link.
Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
The json-logs/samples/api fixtures are schema skeletons the mock server replays (blank strings, placeholder ids), not real responses — see apps.manifest.validate.json. Revert the fixture to that convention: one errors[] entry with every field blanked, including the constraint object (type/expected/got) so deserialization is still exercised. The real contract learned from the live API stays reflected in the model and the remote test, which is where it belongs. Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
- Set the SpecialTier_blocks_validate quota to 60 requests/minute (was an unverified 600 copied from other special tiers). - Call blocks.validate without a token in both tests, matching its unauthenticated contract; exempt it from the mock server's token gate (like api.test) so the recorded fixture is served. - Local BlocksTest now asserts the deserialized errors[]/constraint shape instead of not_authed; the remote test exercises a payload that actually fails validation. Regenerate the mock fixture via the recorder. - Drop the unsourced "hundreds of requests per minute" claim from the tier Javadoc. Verified: test_locally.api.methods.BlocksTest, test_locally.api.MethodsTest, and test_with_remote_apis.methods.blocks_Test all pass (JDK 17). Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
Callers building Block Kit with the SDK's own LayoutBlock builders can now pass them directly instead of hand-serializing to JSON. Add a typed List<LayoutBlock> blocks field and rename the raw-JSON field to blocksAsString, mirroring ChatPostMessageRequest (blocksAsString wins if both are set). Tests exercise both the typed and string paths. Verified: test_locally.api.methods.BlocksTest and test_with_remote_apis.methods.blocks_Test pass (JDK 17). Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
Add typed inputs for the message and view payloads, alongside the existing
*AsString escape hatches (string wins if both are set), matching the blocks
handling and views.open's view/viewAsString precedent.
- view uses the existing View model.
- message uses a small nested MessagePayload{blocks, attachments} rather
than the response-shaped Message model. The accepted shape was verified
against the live blocks.validate endpoint: attachments (with nested
blocks) are validated, whereas text is rejected as an additional property,
so text is intentionally omitted.
Verified: test_locally.api.methods.BlocksTest (typed blocks/message/view +
string paths) and test_with_remote_apis.methods.blocks_Test pass (JDK 17).
Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
…ment
The constraint's expected/got fields are polymorphic: an array of strings
for enum constraints (e.g. ["plain_text","mrkdwn"]), but a number for
length/count constraints (min_length/max_length/max_items) and absent for
all_of. Modeling expected as List<String> caused a Gson JsonSyntaxException
("Expected BEGIN_ARRAY but was NUMBER") whenever a numeric constraint was
returned. Use JsonElement so any shape deserializes; callers inspect it via
getAsInt()/getAsJsonArray()/etc.
Adds a remote test that triggers a max_items constraint (numeric
expected/got) to guard the regression.
Verified: test_locally.api.methods.BlocksTest (4) and
test_with_remote_apis.methods.blocks_Test (3) pass (JDK 17).
Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
Move the docs link to a class-level Javadoc and drop the comment on the getToken() override, matching ApiTestRequest and other unauthenticated request objects. Co-Authored-By: Claude <svc-devxp-claude@slack-corp.com>
zimeg
left a comment
There was a problem hiding this comment.
📝 Callouts for the kind folks checking this out!
| @Data | ||
| @Builder | ||
| public static class MessagePayload { | ||
| private List<LayoutBlock> blocks; | ||
| private List<Attachment> attachments; | ||
| } |
There was a problem hiding this comment.
👁️🗨️ note: This is added to the request class for blocks.validate to bring matching arguments for:
- blocks: blocksAsString
- message: messageAsString
- view: viewsAsString
| private JsonElement expected; | ||
| private JsonElement got; |
There was a problem hiding this comment.
📣 note: These can be handled with a typecheck in calling code but for now we avoid parsing responses into particular types here.
This pull request adds the
blocks.validateWeb API method to the Slack API client for Java.slack.methods().blocksValidate(req)and the async equivalentslack.methodsAsync().blocksValidate(req).blocks.validateis unauthenticated — no token or scopes required — so the request carries no token (modeled likeapi.test).SpecialTier_blocks_validate) per the method reference.Request — typed inputs with string escape hatches
Each of the three payload kinds accepts a typed value or a raw JSON string (the
*AsStringfield; the string wins if both are set), mirroringChatPostMessageRequest/ViewsOpenRequest:List<LayoutBlock>blocksAsStringMessagePayloadmessageAsStringViewviewAsStringMessagePayloadis a small nested{blocks, attachments}type rather than the response-shapedMessagemodel. The accepted shape was verified against the live endpoint:attachments(with nested blocks) are validated, whereastextis rejected as an additional property — sotextis intentionally omitted, and the type is valid-by-construction.Response — validation errors
On a failed validation the response returns
errors[], each withpointer(a JSON pointer to the offending element),code,message, and aconstraintobject. The constraint'sexpected/gotare modeled asJsonElementbecause their shape is polymorphic across constraint types (a string array forenum, a number formin_length/max_length/max_items, or absent forall_of) — a single static type here caused a GsonJsonSyntaxExceptionon numeric constraints.Testing
./mvnw test -pl slack-api-client -Dtest=test_locally.api.methods.BlocksTest— local mock server, sync + async, typed and string paths (4 tests).test_with_remote_apis.methods.blocks_Testexercises the live API: a well-formed payload, a malformed payload (invalid_blocks+ populatederrors[]), and a numeric-constraint case (max_items) that guards the polymorphicexpected/gotdeserialization (3 tests).Companion example
An end-to-end example lives in slack-samples/bolt-java-examples#53, building blocks with the SDK builders and validating them.
Category
Requirements
Please read the Contributing guidelines and Code of Conduct before creating this issue or pull request. By submitting, you agree to those rules.
🤖 Generated with Claude Code