From 9b520848a42894925def30750d133cce5b5b71c5 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Thu, 10 Sep 2026 14:55:05 -0700 Subject: [PATCH 1/3] Allow mrkdwn descriptions on OptionObject (fixes #1471) `OptionObject.description` was hard-typed to `PlainTextObject`, so a `mrkdwn` description on a radio-button or checkbox option was coerced to plain_text on deserialize, losing the markdown. The option-object docs state that radio buttons and checkboxes can use mrkdwn text objects for the description. Widen the field to `TextObject` (the same type already used for `OptionObject.text`), which routes deserialization through the existing polymorphic text-object factory so `mrkdwn` round-trips as a `MarkdownTextObject` and `plain_text` still returns a `PlainTextObject`. The Kotlin DSL gains a `markdownDescription(...)` overload alongside the existing plain_text `description(...)`. Source-compatible: all call sites pass plain_text and the getter's declared type only broadens. Ref: https://docs.slack.dev/reference/block-kit/composition-objects/option-object Co-Authored-By: Claude --- .../block/composition/OptionObjectBuilder.kt | 20 +++++- .../test_locally/block/ActionsBlockTest.kt | 65 +++++++++++++++++++ .../model/block/composition/OptionObject.java | 10 +-- .../api/model/block/BlockKitTest.java | 17 +++++ 4 files changed, 105 insertions(+), 7 deletions(-) diff --git a/slack-api-model-kotlin-extension/src/main/kotlin/com/slack/api/model/kotlin_extension/block/composition/OptionObjectBuilder.kt b/slack-api-model-kotlin-extension/src/main/kotlin/com/slack/api/model/kotlin_extension/block/composition/OptionObjectBuilder.kt index 40cff9187..6f7dcedf5 100644 --- a/slack-api-model-kotlin-extension/src/main/kotlin/com/slack/api/model/kotlin_extension/block/composition/OptionObjectBuilder.kt +++ b/slack-api-model-kotlin-extension/src/main/kotlin/com/slack/api/model/kotlin_extension/block/composition/OptionObjectBuilder.kt @@ -1,7 +1,9 @@ package com.slack.api.model.kotlin_extension.block.composition +import com.slack.api.model.block.composition.MarkdownTextObject import com.slack.api.model.block.composition.OptionObject import com.slack.api.model.block.composition.PlainTextObject +import com.slack.api.model.block.composition.TextObject import com.slack.api.model.kotlin_extension.block.BlockLayoutBuilder import com.slack.api.model.kotlin_extension.block.Builder import com.slack.api.model.kotlin_extension.block.composition.container.SingleTextObjectContainer @@ -14,7 +16,7 @@ class OptionObjectBuilder private constructor( ) : Builder, TextObjectDsl by textContainer { private var value: String? = null private var url: String? = null - private var description: PlainTextObject? = null + private var description: TextObject? = null constructor() : this(SingleTextObjectContainer()) @@ -40,8 +42,9 @@ class OptionObjectBuilder private constructor( } /** - * a line of descriptive text shown below the text field beside the radio button. Maximum length for the text - * object within this field is 75 characters. + * A plain_text text object that defines a line of descriptive text shown below the text field beside a single + * selectable item in a select menu, multi-select menu, checkbox group, radio button group, or overflow menu. + * Maximum length for the text within this field is 75 characters. * * @see Option object documentation */ @@ -49,6 +52,17 @@ class OptionObjectBuilder private constructor( description = PlainTextObject(text, emoji) } + /** + * A mrkdwn text object that defines a line of descriptive text shown below the text field beside a single + * selectable item. Only checkbox group and radio button group items can use mrkdwn formatting. + * Maximum length for the text within this field is 75 characters. + * + * @see Option object documentation + */ + fun markdownDescription(text: String, verbatim: Boolean? = null) { + description = MarkdownTextObject(text, verbatim) + } + override fun build(): OptionObject { return OptionObject.builder() .description(description) diff --git a/slack-api-model-kotlin-extension/src/test/kotlin/test_locally/block/ActionsBlockTest.kt b/slack-api-model-kotlin-extension/src/test/kotlin/test_locally/block/ActionsBlockTest.kt index 2eb9d931b..e2f98cf26 100644 --- a/slack-api-model-kotlin-extension/src/test/kotlin/test_locally/block/ActionsBlockTest.kt +++ b/slack-api-model-kotlin-extension/src/test/kotlin/test_locally/block/ActionsBlockTest.kt @@ -504,4 +504,69 @@ class ActionsBlockTest { val actual = gson.toJsonTree(blocks) assertEquals(expected, actual, "\n$expected\n$actual") } + + @Test + fun `checkboxes with mrkdwn option description`() { + val gson = GsonFactory.createSnakeCase() + val blocks = withBlocks { + actions { + checkboxes { + options { + option { + plainText("Checkbox 1") + markdownDescription("*bold* description") + value("mrkdwn-desc") + } + option { + plainText("Checkbox 2") + description("plain description") + value("plain-desc") + } + } + } + } + } + val original = """ + { + "blocks": [ + { + "type": "actions", + "elements": [ + { + "type": "checkboxes", + "options": [ + { + "text": { + "type": "plain_text", + "text": "Checkbox 1" + }, + "value": "mrkdwn-desc", + "description": { + "type": "mrkdwn", + "text": "*bold* description" + } + }, + { + "text": { + "type": "plain_text", + "text": "Checkbox 2" + }, + "value": "plain-desc", + "description": { + "type": "plain_text", + "text": "plain description" + } + } + ] + } + ] + } + ] + } + """.trimIndent() + val json = gson.fromJson(original, JsonElement::class.java) + val expected = json.asJsonObject["blocks"] + val actual = gson.toJsonTree(blocks) + assertEquals(expected, actual, "\n$expected\n$actual") + } } diff --git a/slack-api-model/src/main/java/com/slack/api/model/block/composition/OptionObject.java b/slack-api-model/src/main/java/com/slack/api/model/block/composition/OptionObject.java index 3a5cc1909..d17ef6639 100644 --- a/slack-api-model/src/main/java/com/slack/api/model/block/composition/OptionObject.java +++ b/slack-api-model/src/main/java/com/slack/api/model/block/composition/OptionObject.java @@ -29,11 +29,13 @@ public class OptionObject { private String value; /** - * A plain_text only text object that defines a line of descriptive text shown - * below the text field beside the radio button. - * Maximum length for the text object within this field is 75 characters. + * A plain_text text object that defines a line of descriptive text shown below + * the text field beside a single selectable item in a select menu, multi-select + * menu, checkbox group, radio button group, or overflow menu. Checkbox group and + * radio button group items can also use mrkdwn formatting. + * Maximum length for the text within this field is 75 characters. */ - private PlainTextObject description; + private TextObject description; /** * A URL to load in the user's browser when the option is clicked. diff --git a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java index 302b73bc5..76fb986e0 100644 --- a/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java +++ b/slack-api-model/src/test/java/test_locally/api/model/block/BlockKitTest.java @@ -5,6 +5,9 @@ import com.slack.api.model.Message; import com.slack.api.model.block.*; import com.slack.api.model.block.composition.ConfirmationDialogObject; +import com.slack.api.model.block.composition.MarkdownTextObject; +import com.slack.api.model.block.composition.OptionObject; +import com.slack.api.model.block.composition.PlainTextObject; import com.slack.api.model.block.element.*; import com.slack.api.model.view.View; import org.junit.Test; @@ -1042,6 +1045,10 @@ public void parseCheckboxes() { " \"text\": \"*this is plain_text text*\",\n" + " \"emoji\": true\n" + " },\n" + + " \"description\": {\n" + + " \"type\": \"plain_text\",\n" + + " \"text\": \"this is a plain_text description\"\n" + + " },\n" + " \"value\": \"value-0\"\n" + " },\n" + " {\n" + @@ -1123,9 +1130,19 @@ public void parseCheckboxes() { CheckboxesElement checkboxes1 = (CheckboxesElement) input.getElement(); assertThat(checkboxes1.getActionId(), is("input-action-id")); + OptionObject plainOption = checkboxes1.getOptions().get(0); + assertThat(plainOption.getDescription(), instanceOf(PlainTextObject.class)); + assertThat(plainOption.getDescription().getType(), is("plain_text")); + assertThat(((PlainTextObject) plainOption.getDescription()).getText(), is("this is a plain_text description")); + SectionBlock block = (SectionBlock) message.getBlocks().get(1); CheckboxesElement checkboxes2 = (CheckboxesElement) block.getAccessory(); assertThat(checkboxes2.getActionId(), is("section-action-id")); + + OptionObject mrkdwnOption = checkboxes2.getOptions().get(0); + assertThat(mrkdwnOption.getDescription(), instanceOf(MarkdownTextObject.class)); + assertThat(mrkdwnOption.getDescription().getType(), is("mrkdwn")); + assertThat(((MarkdownTextObject) mrkdwnOption.getDescription()).getText(), is("*this is mrkdwn text*")); } @Test From 5f44fd2e4cfd818bc9c6d0f117d2737ea96265c4 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Thu, 10 Sep 2026 16:33:00 -0700 Subject: [PATCH 2/3] test: fold mrkdwn option description into existing checkboxes test Exercise markdownDescription() within the existing "Channel select and checkboxes" test rather than a standalone case, mirroring the Java-side fold into parseCheckboxes(). The first checkbox option now asserts the mrkdwn description path while the second keeps the plain_text path. Co-Authored-By: Claude --- .../test_locally/block/ActionsBlockTest.kt | 71 +------------------ 1 file changed, 3 insertions(+), 68 deletions(-) diff --git a/slack-api-model-kotlin-extension/src/test/kotlin/test_locally/block/ActionsBlockTest.kt b/slack-api-model-kotlin-extension/src/test/kotlin/test_locally/block/ActionsBlockTest.kt index e2f98cf26..d6ff0560c 100644 --- a/slack-api-model-kotlin-extension/src/test/kotlin/test_locally/block/ActionsBlockTest.kt +++ b/slack-api-model-kotlin-extension/src/test/kotlin/test_locally/block/ActionsBlockTest.kt @@ -438,7 +438,7 @@ class ActionsBlockTest { checkboxes { options { option { - description("I accept the terms and conditions") + markdownDescription("*I accept the terms and conditions*") value("tac-accept") } option { @@ -481,8 +481,8 @@ class ActionsBlockTest { { "value": "tac-accept", "description": { - "type": "plain_text", - "text": "I accept the terms and conditions" + "type": "mrkdwn", + "text": "*I accept the terms and conditions*" } }, { @@ -504,69 +504,4 @@ class ActionsBlockTest { val actual = gson.toJsonTree(blocks) assertEquals(expected, actual, "\n$expected\n$actual") } - - @Test - fun `checkboxes with mrkdwn option description`() { - val gson = GsonFactory.createSnakeCase() - val blocks = withBlocks { - actions { - checkboxes { - options { - option { - plainText("Checkbox 1") - markdownDescription("*bold* description") - value("mrkdwn-desc") - } - option { - plainText("Checkbox 2") - description("plain description") - value("plain-desc") - } - } - } - } - } - val original = """ - { - "blocks": [ - { - "type": "actions", - "elements": [ - { - "type": "checkboxes", - "options": [ - { - "text": { - "type": "plain_text", - "text": "Checkbox 1" - }, - "value": "mrkdwn-desc", - "description": { - "type": "mrkdwn", - "text": "*bold* description" - } - }, - { - "text": { - "type": "plain_text", - "text": "Checkbox 2" - }, - "value": "plain-desc", - "description": { - "type": "plain_text", - "text": "plain description" - } - } - ] - } - ] - } - ] - } - """.trimIndent() - val json = gson.fromJson(original, JsonElement::class.java) - val expected = json.asJsonObject["blocks"] - val actual = gson.toJsonTree(blocks) - assertEquals(expected, actual, "\n$expected\n$actual") - } } From 6b352a191220c32a53bc72c49329d400e0b98114 Mon Sep 17 00:00:00 2001 From: Eden Zimbelman Date: Thu, 10 Sep 2026 16:47:39 -0700 Subject: [PATCH 3/3] test: set OptionObject descriptions in sample-JSON generator for TextObject field The sample-JSON generator (SampleObjects) reflectively instantiates null model fields via a no-arg constructor. Widening OptionObject.description from PlainTextObject to the abstract TextObject broke this for the radio-button option fixtures that set text but left description null, crashing MethodsResponseDumpTest's static init with InstantiationException. Supply concrete descriptions in those builders (as the generator already does elsewhere), giving the mrkdwn option a mrkdwn description. Regenerates the views.* API samples to show a mrkdwn option description. Co-Authored-By: Claude --- json-logs/samples/api/views.open.json | 4 ++-- json-logs/samples/api/views.publish.json | 4 ++-- json-logs/samples/api/views.push.json | 4 ++-- json-logs/samples/api/views.update.json | 4 ++-- .../java/util/sample_json_generation/SampleObjects.java | 6 +++--- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/json-logs/samples/api/views.open.json b/json-logs/samples/api/views.open.json index 731b3ea0c..895e8f145 100644 --- a/json-logs/samples/api/views.open.json +++ b/json-logs/samples/api/views.open.json @@ -2502,9 +2502,9 @@ }, "value": "", "description": { - "type": "plain_text", + "type": "mrkdwn", "text": "", - "emoji": false + "verbatim": false }, "url": "" } diff --git a/json-logs/samples/api/views.publish.json b/json-logs/samples/api/views.publish.json index 731b3ea0c..895e8f145 100644 --- a/json-logs/samples/api/views.publish.json +++ b/json-logs/samples/api/views.publish.json @@ -2502,9 +2502,9 @@ }, "value": "", "description": { - "type": "plain_text", + "type": "mrkdwn", "text": "", - "emoji": false + "verbatim": false }, "url": "" } diff --git a/json-logs/samples/api/views.push.json b/json-logs/samples/api/views.push.json index 731b3ea0c..895e8f145 100644 --- a/json-logs/samples/api/views.push.json +++ b/json-logs/samples/api/views.push.json @@ -2502,9 +2502,9 @@ }, "value": "", "description": { - "type": "plain_text", + "type": "mrkdwn", "text": "", - "emoji": false + "verbatim": false }, "url": "" } diff --git a/json-logs/samples/api/views.update.json b/json-logs/samples/api/views.update.json index 731b3ea0c..895e8f145 100644 --- a/json-logs/samples/api/views.update.json +++ b/json-logs/samples/api/views.update.json @@ -2502,9 +2502,9 @@ }, "value": "", "description": { - "type": "plain_text", + "type": "mrkdwn", "text": "", - "emoji": false + "verbatim": false }, "url": "" } diff --git a/slack-api-client/src/test/java/util/sample_json_generation/SampleObjects.java b/slack-api-client/src/test/java/util/sample_json_generation/SampleObjects.java index 11d0596a8..7302abab0 100644 --- a/slack-api-client/src/test/java/util/sample_json_generation/SampleObjects.java +++ b/slack-api-client/src/test/java/util/sample_json_generation/SampleObjects.java @@ -262,10 +262,10 @@ private static List initBlocks() { .text(initProperties(PlainTextObject.builder().build())) .build())) .options(Arrays.asList( - initProperties(OptionObject.builder().text(initProperties(PlainTextObject.builder().build())).build()), - initProperties(OptionObject.builder().text(initProperties(MarkdownTextObject.builder().build())).build()) + initProperties(OptionObject.builder().text(initProperties(PlainTextObject.builder().build())).description(initProperties(PlainTextObject.builder().build())).build()), + initProperties(OptionObject.builder().text(initProperties(MarkdownTextObject.builder().build())).description(initProperties(MarkdownTextObject.builder().build())).build()) )) - .initialOption(initProperties(OptionObject.builder().text(initProperties(PlainTextObject.builder().build())).build())) + .initialOption(initProperties(OptionObject.builder().text(initProperties(PlainTextObject.builder().build())).description(initProperties(PlainTextObject.builder().build())).build())) .build()); public static List ModalBlocks = asBlocks(