MultiLoader 1.21+ · Part 26

Custom Enchantments (MultiLoader 1.21+)

INTERMEDIATE MULTILOADER 1.21-1.21.1 22 min read · Nov 2, 2026
MultiLoader 1.21+ · 28 parts
1 Getting Started with MultiLoader 1.21+ 2 Setting Up RegistrationUtils 3 Creating Items (MultiLoader 1.21+) 4 Creating Blocks (MultiLoader 1.21+) 5 Data Generation: Block & Item Models (MultiLoader 1.21+) 6 Data Generation: Block Loot Tables (MultiLoader 1.21+) 7 Data Generation: Crafting Recipes (MultiLoader 1.21+) 8 Data Generation: Block & Item Tags (MultiLoader 1.21+) 9 Custom Food Items (MultiLoader 1.21+) 10 Custom Tools (MultiLoader 1.21+) 11 Custom Armour (MultiLoader 1.21+) 12 Block Entities (MultiLoader 1.21+) 13 Config Files (MultiLoader 1.21+) 14 Custom Sounds (MultiLoader 1.21+) 15 Events and Listeners (MultiLoader 1.21+) 16 Networking and Custom Packets (MultiLoader 1.21+) 17 Data Generation: Advancements (MultiLoader 1.21+) 18 Data Generation: Language Files (MultiLoader 1.21+) 19 Custom Entities (MultiLoader 1.21+) 20 Ore Generation (MultiLoader 1.21+) 21 Introduction to Mixins (MultiLoader 1.21+) 22 Custom Particles (MultiLoader 1.21+) 23 Menus & Screens (MultiLoader 1.21+) 24 Key Bindings (MultiLoader 1.21+) 25 Custom Potion Effects (MultiLoader 1.21+) 26 Custom Enchantments (MultiLoader 1.21+) 27 Custom Commands (MultiLoader 1.21+) 28 Custom Biomes (MultiLoader 1.21+)

Minecraft 1.21 replaced the Java-class-based enchantment system with a fully data-driven one. Custom enchantments are now defined entirely in JSON files (or generated via datagen), with no Java subclass required for most behaviours. Built-in effect types cover the vast majority of useful enchantment mechanics, and a custom Java effect type is only needed for genuinely novel logic that cannot be composed from existing types.

What Changed in 1.21

Before 1.21, you extended the Enchantment class and overrode methods like getDamageBonus and doPostAttack. That API no longer exists. In 1.21.1:

  • Enchantments are registered as datapack built-in entries, similar to biomes and structures.
  • Each enchantment JSON describes its level range, cost formula, applicable items, and a set of effect components.
  • Effect components define what the enchantment does: add damage, apply a mob effect, spawn particles, explode, and so on.
  • Exclusive sets prevent incompatible enchantments from being combined on the same item.

The enchantment is still a registry object and still has a ResourceKey<Enchantment>, but the implementation is driven by data rather than Java logic.

The Enchantment JSON

Create the file src/main/resources/data/examplemod/enchantment/healing_strike.json in your common project. This "Healing Strike" enchantment applies Regeneration to the attacker for a short time whenever they hit a mob, scaling duration with enchantment level:

json
{
"description": {
"translate": "enchantment.examplemod.healing_strike"
},
"exclusive_set": "#minecraft:exclusive_set/damage",
"supported_items": "#minecraft:enchantable/sword",
"primary_items": "#minecraft:enchantable/sword",
"weight": 3,
"max_level": 3,
"min_cost": {
"base": 10,
"per_level_above_first": 8
},
"max_cost": {
"base": 30,
"per_level_above_first": 8
},
"anvil_cost": 4,
"slots": ["mainhand"],
"effects": {
"minecraft:post_attack": [
{
"affected": "attacker",
"effect": {
"type": "minecraft:apply_mob_effect",
"to_apply": ["minecraft:regeneration"],
"min_duration": {
"type": "minecraft:linear",
"base": 2.0,
"per_level_above_first": 1.0
},
"max_duration": {
"type": "minecraft:linear",
"base": 2.0,
"per_level_above_first": 1.0
},
"min_amplifier": 0,
"max_amplifier": 0
}
}
]
}
}

Key fields explained:

  • description: The display name, as a raw text component. Using translate defers to the lang file.
  • exclusive_set: A tag of enchantments that cannot coexist with this one on the same item. Using the vanilla #minecraft:exclusive_set/damage tag prevents combining Healing Strike with Sharpness, Smite, and Bane of Arthropods.
  • supported_items: The tag of items this enchantment can appear on at all. Used by the enchanting table and the /enchant command.
  • primary_items: The subset of supported items the enchanting table prefers. Items in this tag are weighted higher in the enchanting table pool.
  • weight: How often this enchantment appears in the enchanting table. Higher weight means more frequent. Vanilla common enchantments use 10; rarer ones use 1-5.
  • min_cost / max_cost: The required enchanting table level range, calculated as base + (level - 1) * per_level_above_first.
  • anvil_cost: The experience cost in an anvil to apply or combine this enchantment.
  • slots: Which equipment slots activate the enchantment effect. Common values are mainhand, offhand, head, chest, legs, feet, armor, hand.

Effect Triggers and Built-in Effects

The effects object maps trigger keys to lists of conditional effects. Each trigger fires at a specific point in game logic:

  • minecraft:post_attack: After the enchanted item hits an entity. Provides "affected" to target the attacker, victim, or damaging entity.
  • minecraft:damage: Adds bonus damage to each hit.
  • minecraft:hit_block: When a block is hit with the item.
  • minecraft:equipment_drops: Modifies the drop chance of equipment when the carrier dies.
  • minecraft:tick: Fires every tick while the item is in the relevant slot.
  • minecraft:projectile_spawned: After a projectile is fired from the enchanted bow or crossbow.

Within each trigger, the available effect types include:

  • minecraft:apply_mob_effect: Applies one or more mob effects to the target entity.
  • minecraft:damage_item: Deals extra durability damage to the item.
  • minecraft:explode: Creates an explosion at the target's position.
  • minecraft:ignite: Sets the target on fire for a number of seconds.
  • minecraft:play_sound: Plays a sound at the target's position.
  • minecraft:spawn_particles: Spawns a particle effect.
  • minecraft:summon_entity: Summons an entity at the target's position.
  • minecraft:all_of: Composes multiple effects into one, all applied together.

Level-scaling values accept either a plain float (constant across all levels) or a LevelBasedValue object. The most common types are:

json
// Constant: always the same regardless of level
"min_duration": 2.0
// Linear: base + (level - 1) * per_level_above_first
"min_duration": {
"type": "minecraft:linear",
"base": 2.0,
"per_level_above_first": 1.0
}
// Clamped: clamps a linear value between min and max
"min_duration": {
"type": "minecraft:clamped",
"value": { "type": "minecraft:linear", "base": 1.0, "per_level_above_first": 0.5 },
"min": 1.0,
"max": 3.0
}

Registering the Resource Key

Even though the enchantment is defined in JSON, you still need a ResourceKey<Enchantment> in Java so you can reference the enchantment at runtime (for example, to check whether an item has it, or to give it to the player in a loot table). Create an EnchantmentKeys class in your common project:

java
public class EnchantmentKeys {
public static final ResourceKey<Enchantment> HEALING_STRIKE =
ResourceKey.create(
Registries.ENCHANTMENT,
ResourceLocation.fromNamespaceAndPath(Constants.MOD_ID, "healing_strike")
);
}

To retrieve a live Holder<Enchantment> at runtime for use in code (such as checking enchantment level on an item), look it up from the level's registry:

java
// In a context where you have access to a Level or RegistryAccess:
Holder<Enchantment> healingStrike = level.registryAccess()
.lookupOrThrow(Registries.ENCHANTMENT)
.getOrThrow(EnchantmentKeys.HEALING_STRIKE);
// Check if a stack has the enchantment:
int level = EnchantmentHelper.getItemEnchantmentLevel(healingStrike, itemStack);

Datagen via DatapackBuiltinEntriesProvider

Instead of writing the JSON by hand, you can generate it with DatapackBuiltinEntriesProvider. This is recommended for large mods because it keeps the enchantment definition type-safe and co-located with the rest of your datagen code.

First, create a bootstrap method that registers the enchantment using the Java builder API:

java
public class ExampleEnchantmentBootstrap {
public static void bootstrap(BootstrapContext<Enchantment> context) {
HolderGetter<Item> items = context.lookup(Registries.ITEM);
HolderGetter<MobEffect> effects = context.lookup(Registries.MOB_EFFECT);
context.register(
EnchantmentKeys.HEALING_STRIKE,
Enchantment.enchantment(
Enchantment.definition(
items.getOrThrow(ItemTags.SWORD_ENCHANTABLE), // supported items
items.getOrThrow(ItemTags.SWORD_ENCHANTABLE), // primary items
3, // weight
3, // max level
Enchantment.dynamicCost(10, 8), // min cost
Enchantment.dynamicCost(30, 8), // max cost
4, // anvil cost
EquipmentSlotGroup.MAINHAND
)
)
.withEffect(
EnchantmentEffectComponents.POST_ATTACK,
EnchantmentTarget.ATTACKER,
EnchantmentTarget.VICTIM,
new ApplyMobEffect(
HolderSet.direct(MobEffects.REGENERATION),
LevelBasedValue.perLevel(2.0f, 1.0f), // min duration
LevelBasedValue.perLevel(2.0f, 1.0f), // max duration
LevelBasedValue.constant(0), // min amplifier
LevelBasedValue.constant(0) // max amplifier
)
)
.build(EnchantmentKeys.HEALING_STRIKE.location())
);
}
}

Then wire it into your existing DatapackBuiltinEntriesProvider in the NeoForge GatherDataEvent handler. Add Registries.ENCHANTMENT to the set of registries to generate and pass ExampleEnchantmentBootstrap::bootstrap as the population function:

java
generator.addProvider(true,
new DatapackBuiltinEntriesProvider(output, registries,
new RegistrySetBuilder()
.add(Registries.CONFIGURED_FEATURE, ExampleWorldGenProvider::configuredFeatures)
.add(Registries.PLACED_FEATURE, ExampleWorldGenProvider::placedFeatures)
.add(NeoForgeRegistries.Keys.BIOME_MODIFIERS, ExampleWorldGenProvider::biomeModifiers)
.add(Registries.ENCHANTMENT, ExampleEnchantmentBootstrap::bootstrap),
Set.of(Constants.MOD_ID)));
NOTE
When using datagen you do not need the hand-written JSON file in your resources folder. The two approaches are alternatives. If you include both, the generated file will overwrite the hand-written one during a datagen run, which can cause confusion. Pick one approach and stick to it.

Custom Effect Types

If none of the built-in effect types cover your desired behaviour, you can register a custom one. A custom entity effect must implement EnchantmentEntityEffect and provide a MapCodec for JSON serialisation:

java
public record TeleportAttackerEffect(LevelBasedValue maxDistance)
implements EnchantmentEntityEffect {
public static final MapCodec<TeleportAttackerEffect> CODEC =
RecordCodecBuilder.mapCodec(instance -> instance.group(
LevelBasedValue.CODEC.fieldOf("max_distance").forGetter(TeleportAttackerEffect::maxDistance)
).apply(instance, TeleportAttackerEffect::new));
@Override
public void apply(ServerLevel level, int enchantmentLevel,
EnchantedItemInUse item, Entity entity, Vec3 origin) {
if (entity instanceof ServerPlayer player) {
double range = maxDistance.calculate(enchantmentLevel);
// teleport logic here
}
}
@Override
public MapCodec<TeleportAttackerEffect> codec() {
return CODEC;
}
}

Register the codec in a new registry provider:

java
public class EnchantmentEffectRegistry {
public static final RegistrationProvider<MapCodec<? extends EnchantmentEntityEffect>> ENTITY_EFFECTS =
RegistrationProvider.get(Registries.ENCHANTMENT_ENTITY_EFFECT_TYPE, Constants.MOD_ID);
public static final RegistryObject<
MapCodec<? extends EnchantmentEntityEffect>,
MapCodec<TeleportAttackerEffect>> TELEPORT_ATTACKER =
ENTITY_EFFECTS.register("teleport_attacker",
() -> TeleportAttackerEffect.CODEC);
public static void init() {}
}

Then reference it in your enchantment JSON by its registry name:

json
"effects": {
"minecraft:post_attack": [
{
"affected": "attacker",
"effect": {
"type": "examplemod:teleport_attacker",
"max_distance": {
"type": "minecraft:linear",
"base": 5.0,
"per_level_above_first": 3.0
}
}
}
]
}

Lang Entry

Add a single translation entry for the enchantment name. The key format is enchantment.<namespace>.<path>:

java
add("enchantment.examplemod.healing_strike", "Healing Strike");

The description in the JSON uses { "translate": "enchantment.examplemod.healing_strike" }, so this lang key must exist for the name to appear correctly in the enchanting table and on items.

You can also grab this directly from the EnchantmentKeys file where we declared a ResourceKey earlier.

Testing

Use /enchant @s examplemod:healing_strike 3 to apply the enchantment to your held sword at level III. The command will fail if the enchantment JSON is not found or contains a syntax error; check the server log for a more specific error message in that case.

Hit a mob and confirm the Regeneration effect appears on your character. Use /effect give @s minecraft:regeneration 0 first to clear any existing Regeneration, then strike a mob and watch the effect bar for the newly applied one.

To test that the enchanting table respects the weight and level range, place a max-level enchanting table (surrounded by bookshelves) and enchant a sword repeatedly. With a weight of 3, Healing Strike should appear occasionally but less frequently than common enchantments like Sharpness (weight 10). Verify it does not appear on non-sword items by trying to enchant a pickaxe.

You can find the source for this tutorial here:

View Source on GitHub
NEXT IN SERIES

Custom Commands (MultiLoader 1.21+)

Build a Brigadier command tree with literal and argument nodes, handle permission levels via CommandSourceStack, use vanilla argument types including EntityArgument and BlockPosArgument, and register the dispatcher on both loaders.

Continue →