Stop Subclassing, Start Composing
In Java Edition, creating a new mob, block, or item almost always starts with object-oriented inheritance. You extend a base class like Block or PathfinderMob, override a handful of methods to alter its behavior, and register the new class during mod initialization. If you want a zombie that shoots arrows, you either subclass Zombie and graft ranged attack goals onto it, or you write a Mixin to inject logic directly into the vanilla class.
Bedrock Edition has no concept of class inheritance for content creators. Instead, it uses a strict Entity-Component-System (ECS) architecture driven entirely by JSON files. An entity, block, or item is nothing more than an identifier with a bag of components attached to it. Vanilla entities are defined using the exact same JSON format available to you. If you want an entity to walk, you give it the minecraft:movement and minecraft:navigation.walk components. If you want it to be rideable, you attach minecraft:rideable.
To make that same arrow-shooting zombie in Bedrock, you do not write code that extends a zombie. You copy the vanilla zombie JSON file into your behavior pack, remove the minecraft:behavior.melee_attack component, and add minecraft:behavior.ranged_attack and minecraft:shooter. You cannot override the internal C++ implementation of a component; you can only configure the parameters that the engine exposes in JSON, or swap components in and out dynamically using component groups and events.
| Java Edition Concept | Bedrock Edition Equivalent | Architectural Shift |
|---|---|---|
| Extending a base class (e.g., extends Block) | Defining a JSON file with components | Composition over inheritance; all traits are modular components. |
| Bytecode injection (Mixins / ASM) | Overriding vanilla JSON files or listening to Script API events | You cannot alter internal engine methods, only data definitions and exposed hooks. |
| Mod initializer registration (DeferredRegister) | Placing files in the correct pack directory | Content is auto-discovered by folder structure and identifier. |
| Hardcoded AI Goals (GoalSelector) | JSON behavior components and priority weights | AI is assembled declaratively and swapped via entity events. |
| NBT (Named Binary Tag) runtime data | Dynamic Properties and Entity Properties | Arbitrary NBT access does not exist at runtime in Bedrock. |
| Client/Server proxy classes (@SidedProxy) | Physical separation into Behavior Pack (BP) and Resource Pack (RP) | Server logic and client assets never live in the same package. |
There Are No Mixins (And How to Survive Without Them)
The biggest shock for experienced Java modders is the complete absence of bytecode manipulation. In Java, if the modding API does not provide a hook for a specific mechanic, you can decompile the game, find the exact line of code responsible, and write a Mixin to intercept or overwrite it. In Bedrock, the core game is compiled native C++ code. You cannot see it, decompile it, or inject into it. If the engine does not expose a behavior through a JSON component, a Molang query, or a Script API event, you simply cannot touch it directly.
To survive without Mixins, you have to learn how to combine vanilla overrides with the TypeScript/JavaScript Script API. When you want to change how a vanilla mechanic works, your first option is to copy the vanilla entity, block, or loot table JSON into your Behavior Pack using the exact same minecraft: identifier; your file will overwrite the vanilla one. However, overriding vanilla files makes your add-on incompatible with any other add-on that touches the same file.
The cleaner, modern approach is to use the @minecraft/server Script API. Bedrock provides an event-driven scripting environment featuring 'before' and 'after' events. A beforeEvent fires on the server before an action occurs in the C++ engine, allowing you to cancel the action or modify its parameters. An afterEvent fires after the action has completed, allowing you to run follow-up logic. Because scripts run in a restricted sandbox during the beforeEvent phase, you must defer world modifications by a single tick using system.run().
import { world, system } from "@minecraft/server";
// Intercepting block breaking (similar to a Forge BlockEvent.BreakEvent)
world.beforeEvents.playerBreakBlock.subscribe((event) => {
const { player, block, itemStack } = event;
// Prevent breaking diamond ore without a diamond pickaxe
if (block.typeId === "minecraft:diamond_ore") {
if (!itemStack || itemStack.typeId !== "minecraft:diamond_pickaxe") {
// Cancel the native C++ block break directly inside the beforeEvent
event.cancel = true;
// World state changes (like sending messages or spawning particles)
// are restricted in beforeEvents and must be deferred to the main tick.
system.run(() => {
player.sendMessage("Your pickaxe is too weak to mine this ore.");
});
}
}
});Unlearning NBT: State, Properties, and Components
Java moddersand datapack creators alike treat NBT as a universal tool. On Java, you can read an entity's entire NBT compound at runtime, modify its Health tag, read its Inventory list, or attach custom data compounds to an ItemStack on the fly. In Bedrock, although the world save format uses NBT on disk, NBT is completely inaccessible to add-ons at runtime. You cannot use a /data command, you cannot write NBT in command selectors, and you cannot serialize an entity to an NBT object in script.
Instead of a single unified NBT structure, Bedrock splits state and custom data into four strict, purpose-built systems. First, for blocks, you use Block States (defined under states in the block's description) and Permutations, which work similarly to Java's BlockState properties by swapping out component sets when a state changes. Second, for arbitrary persistent data on entities, items, or the world itself, you use Dynamic Properties via the Script API (setDynamicProperty and getDynamicProperty). These store strings, numbers, booleans, or Vector3 values directly on the object.
Third, when you need an entity's custom state to be readable by the client (for example, to change a texture, play an animation, or trigger a render controller), you cannot use script Dynamic Properties because scripts only run on the server. Instead, you must declare Entity Properties in the entity's Behavior Pack JSON with client_sync set to true. Finally, to read or modify vanilla data like health or inventory, you query specific API components in script (such as calling entity.getComponent("minecraft:health")).
{
"format_version": "1.20.80",
"minecraft:block": {
"description": {
"identifier": "wiki:custom_lamp",
"states": {
"wiki:lit": [false, true]
}
},
"components": {
"minecraft:destructible_by_mining": {
"seconds_to_destroy": 1.5
},
"minecraft:geometry": "minecraft:geometry.full_block",
"minecraft:material_instances": {
"*": {
"texture": "custom_lamp_off",
"render_method": "opaque"
}
},
"minecraft:light_emission": 0
},
"permutations": [
{
"condition": "q.block_state('wiki:lit') == true",
"components": {
"minecraft:light_emission": 15,
"minecraft:material_instances": {
"*": {
"texture": "custom_lamp_on",
"render_method": "opaque"
}
}
}
}
]
}
}The Hard Wall Between Behavior Packs and Resource Packs
When you build a Java mod, everything compiles into a single .jar file. Your server-side block logic, your client-side rendering code, your textures, and your language files live side by side in the same archive. Furthermore, it is dangerously easy in Java to accidentally reference client-side classes from server logic, or to run calculations inside a rendering method that rely on internal server state.
Bedrock enforces a physical separation between server and client by dividing every add-on into two completely distinct packages: the Behavior Pack (BP) and the Resource Pack (RP). The Behavior Pack runs purely on the logical server. It contains entity AI, block definitions, loot tables, recipes, functions, and your JavaScript/TypeScript code. The Resource Pack runs purely on the client. It contains models, textures, sounds, animations, particles, and render controllers.
There is no shared memory and no direct code communication between the two packs. Your Behavior Pack script cannot directly tell the client to play a specific keyframe animation or change a model part's visibility. Instead, you must use indirect, synchronized bridges. The server modifies a block state, sets a synced Entity Property, or fires an entity event that applies a skin_id or variant component. The client's Resource Pack then continuously evaluates those synced values using a lightweight expression language called Molang, and updates animations and rendering accordingly.
Stop Ticking Everything: Molang and Event-Driven Design
In Java modding, it is common practice to override a tick() method on a custom Entity or BlockEntity, running procedural logic 20 times a second for every instance in the world. If you carry this habit into Bedrock and use system.runInterval() in the Script API to loop over hundreds of entities or blocks every tick, you will quickly hit script watchdog performance limits and lag the server. Crossing the boundary between the JavaScript engine and the native C++ engine has a measurable performance cost.
Bedrock is designed to push per-tick and per-frame evaluation out of script and into native C++ using two mechanisms: JSON events/components on the server, and Molang on the client. For blocks, rather than ticking every custom block from a central script manager, you attach the minecraft:tick component alongside minecraft:custom_components in the block's JSON, allowing the C++ engine to invoke your script callback only at the specified interval.
On the client side, you never write JavaScript to control visuals. Instead, you write Molang expressions inside your Resource Pack JSON files. Molang is a fast, math-focused expression language compiled and evaluated directly by the C++ engine every render frame. If you want an entity's limbs to swing based on movement speed, or a model part to glow when an entity property changes, you write a Molang query inside an animation or render controller.
{
"format_version": "1.8.0",
"render_controllers": {
"controller.render.custom_mob": {
"geometry": "Geometry.default",
"materials": [
{
"*": "Material.default"
}
],
"textures": [
"Array.skins[q.property('wiki:anger_state')]"
],
"arrays": {
"textures": {
"Array.skins": [
"Texture.calm",
"Texture.angry"
]
}
}
}
}
}Format Versions Are Not Just Metadata
In a Java mod, your mods.toml or fabric.mod.json file declares which version of Minecraft your mod targets. If you update your mod for a newer version of the game, the Java compiler immediately flags any broken method signatures or renamed classes. You fix the compile errors, rebuild the jar, and every class in your mod now runs against the new game version's rules.
In Bedrock, backward compatibility is handled file-by-file using the format_version field at the top of almost every JSON document. This field is not passive metadata; it is an active instruction to the C++ parser telling it which schema and engine behavior to use for that specific file. Because Mojang strives to keep older add-ons working indefinitely without recompilation, the engine maintains multiple parsing paths simultaneously.
If you copy an entity file from an old tutorial that uses a format_version of 1.12.0, and you try to paste a modern component or Entity Property from 1.20.80 into it, the engine will silently ignore the new fields or throw a schema error because the 1.12.0 parser does not recognize them. Conversely, if you bump a file's format_version to the latest release, older deprecated syntax inside that file may suddenly stop working. Always check the format_version of the specific file you are editing, and keep in mind that Resource Pack files (like render controllers at 1.8.0 or attachables at 1.10.0) often use much older version numbers as their current standard than Behavior Pack files.
Which versions this applies to
Applies to all modern Bedrock Edition versions (1.20.80+ and 1.21.x). All concepts, components, and Script API hooks shown here use the stable API and require no experimental toggles.
When it does not work
| What you see | Why | The fix |
|---|---|---|
| ReferenceError: Native function [Player::sendMessage] does not have required privileges. | You attempted to modify world state, spawn an entity, alter a component, or send a chat message directly inside a world.beforeEvents callback. During the 'before' phase, the C++ engine has the world state locked in read-only mode so it can safely evaluate whether the event should be canceled. | Set event.cancel = true directly inside the beforeEvent callback if needed, but wrap any world-mutating methods inside a system.run(() => { ... }) block so they execute on the next tick after the lock is released. |
| Blocks or items appear with a black-and-magenta checker texture, or textures fail to load despite the PNG existing at the exact path. | You put a direct file path (such as "textures/blocks/custom_lamp_on") inside your Behavior Pack block or item JSON. Unlike Java's blockstate and model JSONs, Bedrock Behavior Packs do not know about file paths and only accept short texture aliases. | Define a shortname alias mapped to the file path inside RP/textures/terrain_texture.json (for blocks) or RP/textures/item_texture.json (for items), and reference only that shortname inside your Behavior Pack's minecraft:material_instances or minecraft:icon component. |
| Content Log Error: [Json] Failed to parse file ... or a new component is completely ignored by the game without warning. | The format_version at the top of your JSON file is outdated compared to the components you are trying to use, or you used the minecraft: namespace for your custom identifier. | Update the format_version in the JSON file to match the version when the component was stabilized (for example, 1.20.80 or 1.21.0 for modern blocks and items), and ensure your custom identifier uses a custom namespace (like wiki:custom_lamp) rather than minecraft:. |
| Custom scripts do not execute at all when entering the world, with no errors appearing in the Content Log. | Java mods automatically scan for entrypoints via annotations, whereas Bedrock requires explicit script module and dependency declarations in BP/manifest.json. | Add a module of type "script" with language "javascript" and an "entry" path pointing to your main JS file (e.g., "scripts/main.js") inside BP/manifest.json, and add the "@minecraft/server" module with a valid stable version string under the "dependencies" array. |
Reference, straight from the game files
Read out of the game itself, so the ids match the version you are building for.
