poly.mod.json schema
Put poly.mod.json (schema 1) at the root of the mod jar. The validatePolyModJson task in build and the checker at Poly Mod Loader launch use the same rules. They collect all problems at once and report them as POLY-METADATA-*. All fields are PREVIEW.
Basic fields
| Field | Required | Content |
|---|---|---|
schemaVersion | Yes | 1 |
id | Yes | Mod ID. Lowercase letters, digits, and underscores. The reserved IDs poly, minecraft, and java cannot be used (POLY-DEPENDENCY-008) |
version | Yes | Mod version (SemVer) |
name | Yes | Display name |
environment | Yes | both, client, server |
minecraft | Yes | List of supported Minecraft versions. An exact version ("26.2"), a wildcard ("26.*"), or a range (">=26.2 <27") |
entrypoints | — | A list of class names for each key: common, client, server |
depends | — | Mod ID → version range. Example: "poly": ">=0.1.0-alpha <0.2.0" |
conflicts | — | List of mod IDs that cannot be loaded together (POLY-DEPENDENCY-005) |
Capabilities — capabilities
To use a risky or deep feature, you must declare its permission in capabilities first. A missing required permission gives POLY-METADATA-012, and an unknown permission gives the warning POLY-METADATA-013.
The permission IDs Poly Mod Loader knows today: content.registry.register, gameplay.rules.extend, worldgen.provider.replace, render.pipeline.extend, system.provider.replace, internal.member.access, internal.callsite.intercept, internal.bytecode.transform, runtime.bootstrap.modify, network.protocol.extend, world.data.migrate.
Today Poly Mod Loader actually checks the features below.
| Feature | Required permission |
|---|---|
replacesSystems | system.provider.replace |
transforms | internal.bytecode.transform |
redirect_call in transforms | internal.bytecode.transform, internal.callsite.intercept |
accessWidenings | internal.member.access |
System replacement — replacesSystems
A mod changes the implementation of a game system. The only system you can replace today is poly:game_brand (the game brand string).
{
"capabilities": ["system.provider.replace"],
"replacesSystems": [
{ "system": "poly:game_brand", "provider": "com.example.poly.brand.ExampleBrandProvider" }
]
}
- Format:
{system, provider, override?}.provideris a class that implementsSystemProvider. - Conflicts are resolved before any mod code runs. If there are 2 or more replacements, only one mod wins: the one with
override: truethat (transitively) depends on all the other replacing mods. In every other case, startup stops withPOLY-SYSTEM-001. - The active systems and providers are recorded in the log and in
.poly/startup-report.json.
Example: examples/custom-system-mod in the Poly Mod Loader repository.
Code transforms — transforms
A transform declared by a mod changes game bytecode directly. You must recheck it for every game version. The only supported actions are a closed set reviewed by the engine.
{
"capabilities": ["internal.bytecode.transform"],
"transforms": [
{
"id": "transform_demo_mod:storage_format_name",
"minecraft": "26.2",
"sides": ["client", "dedicated_server"],
"target": {
"owner": "net/minecraft/world/level/storage/LevelStorageSource",
"method": "getName",
"descriptor": "()Ljava/lang/String;"
},
"action": {
"type": "replace_returned_string_constant",
"expected": "Anvil",
"replacement": "Anvil (Poly Mod Loader transform demo)"
},
"onFailure": "disable_rule"
}
]
}
| Field | Content |
|---|---|
id | <mod ID>:<path>. You cannot use another mod's namespace |
minecraft | The exact game version the rule applies to |
sides | client, dedicated_server |
target | owner (internal name), method, descriptor (JVM descriptor) |
action.type | See the action table below |
after | A list of rule IDs that must run first on the same method (optional). You can also list Poly Mod Loader built-in rules (poly:…) |
onFailure | abort_startup or disable_rule (default) |
action.type | Arguments | Meaning |
|---|---|---|
replace_returned_string_constant | expected, replacement | Replaces the constant when the body is exactly LDC <expected>; ARETURN. Cannot be used on the same method as another rule |
invoke_hook_at_head | hookOwner, hookMethod, result (continue, cancel_if_true, return_if_non_null) | Calls a static hook at the start of the method. The hook receives this (if not static) and the arguments |
invoke_hook_before_return | hookOwner, hookMethod | Calls the hook right before every return. The hook receives this, the arguments, and the return value, and returns the value to return |
redirect_call | invokedOwner, invokedName, invokedDescriptor, hookOwner, hookMethod, ordinal (optional, "all" by default or a number counted from 0) | Replaces that call inside the method with a static hook call. The hook receives the receiver (none for a static call) and the arguments, and returns the same type. Also needs the permission internal.callsite.intercept |
Class names accept both com/example/Hooks and com.example.Hooks. A hook's descriptor must match the target exactly. If game types appear, that mod must be compiled against the game jar.
Several rules on the same method
Rules that change the same method (including rules from other mods and Poly Mod Loader's own) are applied together only when after defines a single order. Otherwise all of them are rejected (POLY-TRANSFORM-004).
- Start hooks and before-return hooks run in declaration order. A before-return hook receives the result of the previous hook.
- If a start hook cancels or returns a value, the before-return hooks do not run.
- Two
redirect_callrules that change the same call cannot be combined, and neither can a constant replacement.
Access widening — accessWidenings
Opens a game class, method, or field wider so mod code can use it directly. The meaning is the same as the Fabric access widener. It needs the permission internal.member.access.
{
"capabilities": ["internal.member.access"],
"accessWidenings": [
{
"minecraft": "26.2",
"sides": ["client", "dedicated_server"],
"owner": "net/minecraft/server/MinecraftServer",
"kind": "field",
"name": "tickCount",
"descriptor": "I",
"access": ["accessible"]
}
]
}
| Field | Meaning |
|---|---|
kind | class, method, field. A class has no name or descriptor |
access | accessible (make it public; a private method also becomes non-final), extendable (class or method: remove final; a method becomes at least protected, and calls inside the owner become virtual calls), mutable (field: remove final) |
onFailure | abort_startup (default) or skip. If the target is missing, the error is POLY-TRANSFORM-010 |
-
If several mods widen the same target, the changes are merged and do not conflict.
-
It applies only at run time. A
static finalconstant field has its value copied into the code that reads it, so changing it tomutabledoes not affect that code. -
Mod rules go through the same planning stage as Poly Mod Loader built-in rules: target exists, descriptor, bytecode preconditions, game version, and conflicts with all other rules.
-
If a mod changes the same method as a Poly Mod Loader built-in rule, both rules are rejected (
POLY-TRANSFORM-004).
Example: examples/transform-demo-mod in the Poly Mod Loader repository.
Reserved fields
| Field | Status |
|---|---|
provides | Reserved. If it is not empty, it is rejected with POLY-METADATA-010 |
An unknown field gives the warning POLY-METADATA-011.