Skip to content

Instantly share code, notes, and snippets.

@Soheab
Last active July 18, 2026 15:14
Show Gist options
  • Select an option

  • Save Soheab/ab7a833725f95a84a8f7fa17995cb36c to your computer and use it in GitHub Desktop.

Select an option

Save Soheab/ab7a833725f95a84a8f7fa17995cb36c to your computer and use it in GitHub Desktop.
Components V2 to discord.py' LayoutView

Components V2 to discord.py' LayoutView

A small helper class for discord.py that builds a discord.ui.LayoutView straight from a raw Components V2 payload (the plain dict/JSON shape Discord itself sends and expects), and converts it back.

Normally you build a LayoutView by instantiating the discord.ui items yourself (discord.ui.Container(...), discord.ui.Section(...), etc) and adding them to the view with add_item. That's fine when you're writing the layout by hand, but it gets annoying if you want to store layouts somewhere (a database, a JSON file, a layout editor) and load them dynamically. This handler bridges that gap: give it the dict, get back a real view.

Useful if you're building something like a no-code embed/layout builder, saving user-made layouts to a database, or just want to round-trip a layout you received back into something editable.

How it works

On init, it builds two lookup tables keyed by Discord's component type IDs:

  • one mapping each type ID to the private method that knows how to build that specific discord.ui item
  • one just for selects, since types 3 and 5-8 are all "select menus" but map to different classes (Select, UserSelect, RoleSelect, MentionableSelect, ChannelSelect)

from_dict walks the list of component dicts and adds whatever gets built to a LayoutView. Containers, sections, and action rows all have their own nested components list, so those methods recurse back into the same lookup table to build their children.

to_dict is just a thin wrapper around view.to_components(), which discord.py already gives you. It's mostly there so you have one object with both directions on it.

Component types it understands

ID Component
1 Action Row
2 Button
3 Select (string select)
5 User Select
6 Role Select
7 Mentionable Select
8 Channel Select
9 Section
10 Text Display
11 Thumbnail
12 Media Gallery
13 File
14 Separator
17 Container

Example

Say you've got this payload — a container with some text, a section with a thumbnail accessory, and an action row with a user select:

data = [
    {
        "type": 17,  # Container
        "accent_color": 0x00FF00,
        "spoiler": False,
        "components": [
            {"type": 10, "content": "Hello world"},
            {
                "type": 9,  # Section
                "components": [{"type": 10, "content": "Section text"}],
                "accessory": {
                    "type": 11,  # Thumbnail
                    "media": {"url": "https://example.com/img.png"},
                    "description": "alt text",
                    "spoiler": False,
                },
            },
            {
                "type": 1,  # Action Row
                "components": [
                    {
                        "type": 5,  # User Select
                        "custom_id": "usersel",
                        "placeholder": "Pick a user",
                    }
                ],
            },
        ],
    }
]

Turning that into an actual view is just:

handler = DictLayoutViewHandler(data)
view = handler.from_dict(data)

await channel.send(view=view)

And getting the dict back out again:

raw = handler.to_dict(view)

which gives you back the same structure, just normalized (defaults filled in, keys reordered, etc):

[
    {
        "type": 17,
        "accent_color": 65280,
        "spoiler": False,
        "components": [
            {"type": 10, "content": "Hello world"},
            {
                "type": 9,
                "components": [{"type": 10, "content": "Section text"}],
                "accessory": {
                    "type": 11,
                    "spoiler": False,
                    "media": {"url": "https://example.com/img.png"},
                    "description": "alt text",
                },
            },
            {
                "type": 1,
                "components": [
                    {
                        "type": 5,
                        "custom_id": "usersel",
                        "min_values": 1,
                        "max_values": 1,
                        "disabled": False,
                        "required": True,
                        "placeholder": "Pick a user",
                    }
                ],
            },
        ],
    }
]
from __future__ import annotations
from typing import Any, NotRequired, Required, TypedDict
import discord
class BaseComponent(TypedDict, total=False):
type: Required[int]
components: NotRequired[list[BaseComponent]]
id: NotRequired[int]
custom_id: NotRequired[str]
Select = (
discord.ui.Select
| discord.ui.ChannelSelect
| discord.ui.RoleSelect
| discord.ui.MentionableSelect
| discord.ui.UserSelect
)
class DictLayoutViewHandler:
def __init__(
self,
) -> None:
self._type_to_method = {
1: self._actionrow_from_dict,
2: self._button_from_dict,
3: self._select_from_dict,
5: self._select_from_dict,
6: self._select_from_dict,
7: self._select_from_dict,
8: self._select_from_dict,
9: self._section_from_dict,
10: self._textdisplay_from_dict,
11: self._thumbnail_from_dict,
12: self._mediagallery_from_dict,
13: self._file_from_dict,
14: self._separator_from_dict,
17: self._container_from_dict,
}
self._select_type_to_cls: dict[int, type[Select]] = {
3: discord.ui.Select,
5: discord.ui.UserSelect,
6: discord.ui.RoleSelect,
7: discord.ui.MentionableSelect,
8: discord.ui.ChannelSelect,
}
def to_dict(self, view: discord.ui.LayoutView) -> list[BaseComponent]:
return view.to_components() # type: ignore
def from_dict(self, data: list[BaseComponent], view: discord.ui.LayoutView | None = None) -> discord.ui.LayoutView:
view = view or discord.ui.LayoutView()
for component_data in data:
component_type = component_data['type']
method = self._type_to_method.get(component_type)
if not method:
raise ValueError(f'Unknown component type: {component_type}')
view.add_item(method(component_data))
return view
def _from_emoji_dict(self, data: dict[str, Any]) -> discord.PartialEmoji | None:
if not data:
return None
return discord.PartialEmoji(
name=data['name'],
id=int(emoji_id) if (emoji_id := data.get('id')) else None,
animated=data.get('animated', False),
)
def _button_from_dict(self, data: BaseComponent) -> discord.ui.Button:
return discord.ui.Button(
style=discord.ButtonStyle(data.get('style', 2)),
label=data.get('label'),
custom_id=data.get('custom_id'),
url=data.get('url'),
disabled=data.get('disabled', False),
emoji=self._from_emoji_dict(data.get('emoji', {})),
id=data.get('id'),
)
def _actionrow_from_dict(self, data: BaseComponent) -> discord.ui.ActionRow:
row = discord.ui.ActionRow(id=data.get('id'))
for component_data in data.get('components', []):
component_type = component_data['type']
method = self._type_to_method.get(component_type)
if not method:
raise ValueError(f'Unknown component type: {component_type}')
row.add_item(method(component_data))
return row
def _select_from_dict(self, data: BaseComponent) -> Select:
cls = self._select_type_to_cls.get(data['type'])
if not cls:
raise ValueError(f'Unknown select component type: {data["type"]}')
default_values_data = data.get('default_values')
if default_values_data:
default_values = []
for dtype, did in default_values_data.items():
dtype = discord.SelectDefaultValueType(dtype)
default_values.append(discord.SelectDefaultValue(type=dtype, id=did))
else:
default_values = None
channel_types = data.get(
'channel_types',
)
if channel_types:
channel_types = [discord.ChannelType(channel_type) for channel_type in channel_types]
options_data = data.get('options')
if options_data:
options = []
for option_data in options_data:
options.append(
discord.SelectOption(
label=option_data.get('label', ''),
value=option_data.get('value', ''),
description=option_data.get('description'),
emoji=self._from_emoji_dict(option_data.get('emoji', {})),
default=option_data.get('default', False),
)
)
else:
options = None
extras_per_types = {}
if options:
extras_per_types['options'] = options
if default_values:
extras_per_types['default_values'] = default_values
if channel_types:
extras_per_types['channel_types'] = channel_types
return cls(
custom_id=data.get('custom_id', ''),
placeholder=data.get('placeholder'),
min_values=data.get('min_values', 1),
max_values=data.get('max_values', 1),
disabled=data.get('disabled', False),
id=data.get('id'),
**extras_per_types,
)
def _section_from_dict(self, data: BaseComponent) -> discord.ui.Section:
children = []
for component_data in data.get('components', []):
component_type = component_data['type']
method = self._type_to_method.get(component_type)
if not method:
raise ValueError(f'Unknown component type: {component_type}')
children.append(method(component_data))
accessory: BaseComponent = data.get('accessory', {})
accessory_type = accessory.get('type')
accessory_method = self._type_to_method.get(accessory_type)
if not accessory_method:
raise ValueError(f'Unknown accessory type: {accessory_type}')
return discord.ui.Section(*children, accessory=accessory_method(accessory), id=data.get('id'))
def _mediagallery_from_dict(self, data: BaseComponent) -> discord.ui.MediaGallery:
children = []
items = data.get('items', [])
for item in items:
item = discord.MediaGalleryItem(
media=item['media']['url'], description=item.get('description'), spoiler=item.get('spoiler', False)
)
children.append(item)
return discord.ui.MediaGallery(*children, id=data.get('id'))
def _thumbnail_from_dict(self, data: BaseComponent) -> discord.ui.Thumbnail:
return discord.ui.Thumbnail(
media=data.get('media', {})['url'],
description=data.get('description'),
spoiler=data.get('spoiler', False),
id=data.get('id'),
)
def _file_from_dict(self, data: BaseComponent) -> discord.ui.File:
return discord.ui.File(media=data.get('media', {})['url'], spoiler=data.get('spoiler', False), id=data.get('id'))
def _separator_from_dict(self, data: BaseComponent) -> discord.ui.Separator:
spacing = discord.SeparatorSpacing(data.get('spacing', 1))
return discord.ui.Separator(visible=data.get('divider', True), spacing=spacing, id=data.get('id'))
def _container_from_dict(self, data: BaseComponent) -> discord.ui.Container:
accent_color = discord.Color(accent_color) if (accent_color := data.get('accent_color')) else None
children = []
for component_data in data.get('components', []):
component_type = component_data['type']
method = self._type_to_method.get(component_type)
if not method:
raise ValueError(f'Unknown component type: {component_type}')
children.append(method(component_data))
return discord.ui.Container(
*children, id=data.get('id'), spoiler=data.get('spoiler', False), accent_color=accent_color
)
def _textdisplay_from_dict(self, data: BaseComponent) -> discord.ui.TextDisplay:
return discord.ui.TextDisplay(content=data.get('content', ''), id=data.get('id'))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment