Custom Converters
When the bundled converters don't meet your needs, you can create your own. Kord Extensions provides utilities that make creating your own converters easier, generating converter builders functions automatically.
Build Configuration
Before getting started, make sure you're using our Gradle plugin, our bot template or plugin template, or you set your project up manually according to our tutorial.
Our annotation processor uses these coordinates:
dev.kordex:annotation-processor:
Anatomy
Implementation
All converters extend one of the converter base types:
SingleConverter- This is the type most converters should inherit. Always pick this unless you have a specific reason not to.ChoiceConverter- Type representing a choice-variant converter. These converters are like single converters, but include achoicesmap. They're meant primarily to be used with slash commands, but do still work with chat commands.CoalescingConverter- Type representing a coalescing-variant converter. These converters are separate from single converters, so pick this type if you're writing one.DefaultingConverter- Type representing a defaulting-variant converter. Most converters don't extend this directly, instead relying on theCoaleascingToDefaultingConverterandSingleToDefaultingConverterwrapping types, generated via the@Converterannotation.ListConverter- Type representing a list-variant converter. Most converters don't extend this directly, instead relying on theSingleToListConverterwrapping type, generated via the@Converterannotation.OptionalConverter- Type representing an optional-variant converter. Most converters don't extend this directly, instead relying on theCoaleascingToOptionalConverterandSingleToOptionalConverterwrapping types, generated via the@Converterannotation.
All converter base types take a generic type parameter, representing the final type your converter will transform values into.
Once you've picked a base type, you'll need to create a class extending it and implement the required APIs.
@Converter(
"snowflake",
types = [ConverterType.DEFAULTING, ConverterType.LIST, ConverterType.OPTIONAL, ConverterType.SINGLE]
)
public class SnowflakeConverter(
override var validator: Validator<Snowflake> = null,
) : SingleConverter<Snowflake>() {
override val signatureType: Key = CoreTranslations.Converters.Snowflake.signatureType
// ...
}
Your converter must implement all of the APIs specified below, aside from any optional properties with a default value.
Validator provided by other developers.
Translation key referring to a short, non-title-case name for the type of data your converter handles.
For example, number, ID, regex, locale name/code, etc.
This is used by the help command in your help extension,
and shown in error responses.
Translation key referring to a longer description for the type of data your converter handles, if required.
For example, yes or no for Boolean converters.
If provided, this is used instead of signatureType in "invalid value" error messages.
Whether the signatureType property should be shown by the help command in your
help extension.
Set this to false to hide it.
String parsing function used when handling chat command arguments, as explained below.
Tokenising string parser containing this command invocation's string arguments.
May be null when the argument was provided using keyword syntax, or when values are provided by a wrapping
converter.
Command context representing this command invocation.
Do not try to cast this to a simpler type - stick with the basic APIs available on this type.
This is important because Kord Extensions my pass unusual CommandContext subtypes into this function for
specific, niche use-cases.
If the command invocation contains this argument as a keyword argument, this argument won't be null and you
should use it instead of trying to consume tokens from the parser.
Discord option parsing function used when handling slash command arguments, as explained below.
Command context representing this command invocation.
Do not try to cast this to a simpler type - stick with the basic APIs available on this type.
This is important because Kord Extensions my pass unusual CommandContext subtypes into this function for
specific, niche use-cases.
Discord option object, representing the data Discord provided for this argument's value.
You'll need to try to cast this to the type you expect, based on what your converter returns from
toSlashOption.
Conversion function that takes an Argument type, and converts it to a Discord option type, as explained below.
The Argument object you'll need to convert to a Discord option.
Parsing
Your converter must implement parsing for both slash command and chat command arguments.
Slash Commands
Your bot will call the parseOption function to convert slash command arguments into the correct rich type,
and toSlashOption to convert Argument objects to the correct slash command argument type that Discord should use.
These functions together handle your converter's slash commands workflow, and they should follow this outline:
- First, define
toSlashOption, converting theArgumentto the correctOptionWrappertype.- We recommend starting with the return type, which should be
OptionWrapper<T>whereTis one of Kord'sOptionValuesubtypes. - Then, call
wrapOption(name, description) { ... }, settingrequired = trueunless you're explicitly creating an always-optional converter type. Remember to return the resulting value from this function call!
- We recommend starting with the return type, which should be
- Then, create the
parseOptionfunction, and note that theoptionargument must take anOptionValue<*>rather than a more specific type.- Start by casting this to the correct
Kord
OptionValuesubtype usingas?, and bail out viareturn falseif this cast returnsnull. - Attempt to parse the value, throwing a
DiscordRelayedExceptionif you need to return a specific error.- Return
falseto signal that your converter couldn't parse any data, and respond with a generic error.
- Return
- Store the parsed value in
this.parsed.- Return
trueto signal that parsing was a success.
- Return
- Start by casting this to the correct
Kord
When put together, your code should look something like this:
override suspend fun toSlashOption(arg: Argument<*>): OptionWrapper<StringChoiceBuilder> =
wrapOption(arg.displayName, arg.description) {
required = true
}
override suspend fun parseOption(context: CommandContext, option: OptionValue<*>): Boolean {
val optionValue = (option as? StringOptionValue)?.value ?: return false
try {
this.parsed = Snowflake(optionValue)
} catch (_: NumberFormatException) {
throw DiscordRelayedException(
CoreTranslations.Converters.Snowflake.Error.invalid
.withContext(context)
.withOrdinalPlaceholders(optionValue)
)
}
return true
}
Chat Commands
Your bot will call the parse function to parse chat command arguments into the correct rich type.
This function operates on a stream of string-based tokens, as provided by Kord Extensions'
tokenising string parser (TODO).
Your parsing workflow must follow this outline:
- If
named != null, use this as your parsing value.- For single converters, call
parser.parseNext()to retrieve the next value to parse. - For list/coalescing converters, you can also use
parser.peekNext()to retrieve the next value without advancing the cursor.
- For single converters, call
- Attempt to parse the value, throwing a
DiscordRelayedExceptionif you need to return a specific error.- Return
false(for single converters), or0(for list/coalescing converters), to signal that your converter couldn't parse any data, and respond with a generic error.
- Return
- Store the parsed value in
this.parsed.- Return
true(for single converters`), or the number of arguments your converter parsed (for list/coalescing converters), to signal that parsing was a success.
- Return
When put together, your code should look something like this:
override suspend fun parse(parser: StringParser?, context: CommandContext, named: String?): Boolean {
val arg: String = named // Try the keyword argument first.
?: parser?.parseNext()?.data // Try the string parser next.
?: return false // If both are null, bail out.
try {
this.parsed = Snowflake(arg)
} catch (_: NumberFormatException) {
throw DiscordRelayedException(
CoreTranslations.Converters.Snowflake.Error.invalid
.withContext(context)
.withOrdinalPlaceholders(arg)
)
}
return true
}
@Converter Annotation
Now you've written your converter, it is time to generate all the builders and functions your users will need to define
arguments with it.
To do that, you'll need to annotate your class with @Converter, and provide the relevant arguments to it.
@Converter(
"snowflake",
types = [ConverterType.SINGLE, ConverterType.DEFAULTING, ConverterType.LIST, ConverterType.OPTIONAL]
)
Required Arguments
Converter names, used to generate the corresponding builder functions.
You can specify multiple names here to generate multiple sets of builder functions, which might be useful when
dealing with names that may differ in different locales - for example, colour and color.
Converter types to generate builders and builder functions for, following these rules:
- You must specify exactly one of
SINGLEorCOALESCING- not both. - Choice converters must also specify
CHOICE. - You can also provide any combination of
DEFAULTING,LISTandOPTIONALto generate the corresponding wrapping converters.
Optional Arguments
Extra imports your converter requires, which will be included (along with the default ones) in all generated files.
imports = [
"dev.kordex.core.commands.converters.impl.getEnum",
"dev.kordex.core.commands.application.slash.converters.ChoiceEnum",
"java.util.Locale",
]
Arguments added to the generated builder types' constructors, including name, type, visibility modifier and val/var.
By default, Kord Extensions will also pass these into your converter's constructor, but you can prefix them with
!! to prevent this.
builderConstructorArguments = [
"public var getter: suspend (String, Locale) -> E?",
"!! argMap: Map<Key, E>",
]
Generic type parameter used by the generated builder types.
This may either be full definitions including the names and type bounds, or just names if you provide the type
bounds via builderSuffixedWhere.
// Full definition
builderGeneric = "E: Enum<E>"
// Name only
builderGeneric = "E",
builderSuffixedWhere = "E : Enum<E>, E : ChoiceEnum"
Extra properties defined within the generated builder types, including name, type, visibility modifier and
val/var.
Required properties should use lateinit var.
Otherwise, provide a reasonable default value.
builderFields = [
// Required properties:
"public lateinit var radix: Int",
// Optional properties:
"public var maxLength: Int? = null",
"public var minLength: Int? = null",
]
Extra type bounds for the generic type parameters defined via builderGeneric, provided after where.
builderSuffixedWhere = "T: List<*>"
Extra lines of code added to generated builder types' build functions, before constructing the converter
object.
Extra lines of code added to generated builder types' build functions, after constructing the converter
object.
Extra lines of code added to generated builder types' init { } blocks.
builderInitStatements = [
"choices(argMap)",
]
Extra lines of code added to generated builder types, after their init { } blocks and fields, but before their
functions.
builderExtraStatements = [
"/** Add a channel type to the set of types the given channel must match. **/",
"public fun requireChannelType(type: ChannelType) {",
" requiredChannelTypes.add(type)",
"}"
]
Arguments to add to the generated builder functions, passed into the generated builder types' constructors, including name, and type.
functionBuilderArguments = [
"getter = ::getEnum",
]
Generic type parameter used by the generated builder types.
This may either be full definitions including the names and type bounds, or just names if you provide the type
bounds via functionSuffixedWhere.
// Full definition
functionGeneric = "E: Enum<E>"
// Name only
functionGeneric = "E",
functionSuffixedWhere = "E : Enum<E>, E : ChoiceEnum"
Extra type bounds for the generic type parameters defined via functionGeneric, provided after where.
functionSuffixedWhere = "T: List<*>"
Generated Layout
The code example below is what would normally be generated for the bundled Boolean converter, and it should give
you an idea of where each property inserts code into the generated files.
package dev.kordex.core.commands.converters.impl
// Original converter class, for safety
import dev.kordex.core.commands.converters.impl.BooleanConverter
// Imports that all converters need
import dev.kordex.core.InvalidArgumentException
import dev.kordex.core.annotations.UnexpectedFunctionBehaviour
import dev.kordex.core.commands.Arguments
import dev.kordex.core.commands.converters.*
import dev.kordex.core.commands.converters.builders.*
import dev.kordex.core.i18n.types.*
import dev.kord.common.annotation.KordPreview
// Converter type params
import kotlin.Boolean
/** @inject: imports **/
/**
* Builder class for boolean converters. Used to construct a converter based on the given options.
*
* @see BooleanConverter
*/
public class BooleanConverterBuilder /** @inject: builderGeneric **/ (
/** @inject: builderConstructorArguments **/
) : ConverterBuilder<Boolean>() /** @inject: builderSuffixedWhere **/ {
/** @inject: builderFields **/
init {
/** @inject: builderInitStatements **/
}
/** @inject: builderExtraStatements **/
public override fun build(arguments: Arguments): SingleConverter<Boolean> {
/** @inject: builderBuildFunctionPreStatements **/
val converter = BooleanConverter(
validator = validator,
)
/** @inject: builderBuildFunctionStatements **/
return arguments.arg(
displayName = name,
description = description,
converter = converter.withBuilder(this)
)
}
}
/**
* Converter creation function: boolean single converter
*
* @see BooleanConverterBuilder
*/
public fun /** @inject: functionGeneric **/ Arguments.boolean(
/** @inject: functionBuilderArguments **/
body: BooleanConverterBuilder.() -> Unit
): SingleConverter<Boolean> /** @inject: functionSuffixedWhere **/ {
val builder = BooleanConverterBuilder( /** @inject: functionBuilderArguments **/ )
body(builder)
builder.validateArgument()
return builder.build(this)
}
The annotation processor generates code when you run the build task, placing it in your project's build/ folder,
under generated/ksp/main/kotlin/.
Kord Extensions aims to generate well-formatted code, including comments explaining where it injects code, to try to
make everything easier to understand.
If you need more examples, please take a look at the code for the bundled converters.
Usage
You can use your custom converters just like any of the bundled ones, as described on the converter basics page. Create an Arguments subtype, define arguments using the corresponding builder functions, and use it in your command definitions.
If you're developing a library, your users will also be able to use any custom converters you develop for it.