project.json reference
This is the one file pluggy reads. It lives at the repo root, or at the root of each workspace in a monorepo. pluggy walks up from the current directory until it finds one, then walks back down (if workspaces are declared) to classify which workspace you’re sitting in.
Every field is documented below. If a term is unfamiliar, check the glossary.
Shape
{ "name": "my_plugin", "version": "1.0.0", "description": "A small Paper plugin", "authors": ["Alice", "Bob"], "main": "com.example.myplugin.Main", "compatibility": { "versions": ["1.21.8"], "platforms": ["paper"] }, "registries": [ { "url": "github:my-org/private", "credentials": { "username": "${GITHUB_ACTOR}", "password": "${GITHUB_TOKEN}" } } ], "dependencies": { "worldedit": { "source": "modrinth:worldedit", "version": "7.3.15" }, "adventure-api": { "source": "maven:net.kyori:adventure-api", "version": "4.17.0" } }, "shading": { "adventure-api": { "include": ["net/kyori/adventure/**"], "exclude": ["net/kyori/adventure/text/serializer/gson/**"] } }, "resources": { "config.yml": "src/config.yml", "lang/": "src/lang" }, "jdk": { "major": 21 }, "dev": { "port": 25565, "memory": "2G", "onlineMode": false, "jvmArgs": ["-XX:+UseG1GC"], "serverProperties": { "view-distance": 6, "spawn-protection": 0 }, "extraPlugins": ["./plugins/helper.jar"], "hotswap": { "jdk": "jbr", "fallback": "reload" } }, "workspaces": []}No field in this example is unique to that structure. You’ll see them one at a time below.
Fields
name (required)
String matching ^[a-zA-Z0-9_]+$. Becomes the plugin’s name in the generated descriptor (plugin.yml, bungee.yml, velocity-plugin.json, or META-INF/sponge_plugins.json) and the output jar stem (<name>-<version>.jar). doctor enforces the regex; init rejects other characters.
version (required)
String matching ^\d+\.\d+\.\d+(-[a-zA-Z0-9]+)?$. Semver with an optional
prerelease tag, no build metadata. Embedded in the descriptor and the
output filename.
description (optional)
Free-form string. Rendered into the descriptor when non-empty, and inherited from the workspace root when a workspace omits it.
authors (optional)
Array of strings. In the Bukkit descriptor family each name becomes a YAML list entry under authors:. BungeeCord uses a single author: field joined with ", ".
main (required for plugin workspaces)
Fully-qualified Java class name, at least package.Class. pluggy uses this for three things:
- The descriptor’s
mainfield. ${project.className}and${project.packageName}template substitution in resource files and the initial Java class.- The output directory layout inside
src/duringinit(src/com/example/myplugin/Main.java).
Required for every buildable workspace. A workspace-less root that declares workspaces is not buildable itself and can omit main.
compatibility (required)
"compatibility": { "versions": ["1.21.8"], "platforms": ["paper"]}versions: non-empty array of Minecraft versions. Both the legacy1.21.8shape and Mojang’s 2026 calendar scheme (26.1.2) are accepted. The first entry is the primary version. It drives the platform API download,api-versionin the Bukkit descriptor, and the JDK picker for IntelliJ. Forvelocityandsponge, this still reads as a Minecraft version. pluggy resolves the actualvelocity-apiorspongeapiMaven coordinate internally to the latest stable release.platforms: non-empty array. The first entry is the primary platform. Every platform in the array must share the same descriptor family (sameplugin.yml,bungee.yml,velocity-plugin.json, orMETA-INF/sponge_plugins.jsontarget). Mixing families fails early with “Split them into separate workspaces, one per family.”.
The full platform roster ships with the binary:
| id | descriptor | Maven coordinate |
|---|---|---|
paper | plugin.yml | io.papermc.paper:paper-api |
folia | plugin.yml | dev.folia:folia-api |
spigot | plugin.yml | org.spigotmc:spigot-api (SNAPSHOT) |
bukkit | plugin.yml | org.spigotmc:spigot-api (SNAPSHOT) |
velocity | velocity-plugin.json | com.velocitypowered:velocity-api |
waterfall | bungee.yml | io.github.waterfallmc:waterfall-api |
travertine | bungee.yml | (no Maven API; compile against waterfall) |
sponge | META-INF/sponge_plugins.json | org.spongepowered:spongeapi |
Paper handles version strings in two formats. For 1.17 to 1.21.x the artifact is <version>-R0.1-SNAPSHOT. For 26.x and later (Mojang’s calendar scheme: 26.1, 26.1.1, 26.1.2) it’s <version>.build.<N>-alpha. pluggy fetches PaperMC’s maven-metadata.xml and picks the highest matching entry, so you write the plain Minecraft version and pluggy works out the rest.
Spigot and Bukkit go through BuildTools, which decompiles the Mojang server jar using a JDK from pluggy’s cache. Different Minecraft releases require different Java versions (Minecraft 1.21.x allows Java 21 to 26; Minecraft 26.1.x requires Java 25 to 26). pluggy provisions a matching JDK from the Foojay Disco API on first build, so the version you pick at init time isn’t constrained by your host Java.
Sponge surfaces Minecraft versions in the same shape as velocity. pluggy fetches the SpongeVanilla artifact list and resolves the matching SpongeAPI Maven coordinate internally. Modding-specific variants (SpongeForge, SpongeNeo) aren’t modelled. Only the standalone SpongeVanilla server is supported.
registries (optional)
Array of entries. Each entry is either:
- a bare URL string, or
- an object
{ "url": "...", "credentials": { "username": "...", "password": "..." } }.
The URL field accepts an alias as a shorthand: github:owner/repo expands to https://maven.pkg.github.com/owner/repo.
Registries apply to Maven dependencies. Maven Central (https://repo1.maven.org/maven2/) is appended automatically, so artifacts hosted there resolve without any explicit registries entry. The Modrinth API is implicit and doesn’t need declaring either. When credentials are set, list prints [authenticated] next to the URL but never surfaces the values themselves. They’re read when the Maven resolver needs them.
The list is deduplicated by URL (trailing-slash variants count as the same). In a monorepo, the root’s registries are unioned with each workspace’s registries. Duplicates drop.
dependencies (optional)
Object keyed by dependency name. Each value is one of two shapes.
Short form: Modrinth slug shorthand.
"dependencies": { "worldedit": "7.3.15"}Expands to modrinth:worldedit@7.3.15. The key is the slug.
Long form: explicit source.
"dependencies": { "adventure-api": { "source": "maven:net.kyori:adventure-api", "version": "4.17.0" }}source accepts four schemes:
modrinth:<slug>: slug matches^[a-z0-9][a-z0-9\-_]*$.maven:<groupId>:<artifactId>: both match^[a-zA-Z][\w.-]*$.file:<path>: absolute or repo-root-relative.workspace:<name>: sibling workspace. See Workspaces.
The version field shape depends on the scheme:
| Scheme | version meaning |
|---|---|
modrinth | Exact version number as listed on Modrinth, or "*" for latest. |
maven | Maven version. Soft pins (1.2.3) and lower-bounded ranges ([1.2,2.0)) work; property expansion does not. |
file | Arbitrary label. The content-addressed integrity hash is what actually identifies the file. |
workspace | Ignored. The sibling’s own project.json:version wins. |
testDependencies (optional)
Same shape and grammar as dependencies: short or long form, every source kind. These are added to the test classpath only and never end up in the built jar.
"testDependencies": { "assertj": { "source": "maven:org.assertj:assertj-core", "version": "3.26.3" }}Resolved against registries plus an implicit Maven Central, so JUnit itself can always be fetched. JUnit Platform Console Standalone is auto-injected by pluggy test. You never declare it.
testDependencies is read by pluggy test. Other commands ignore it.
shading (optional)
Object keyed by dependency name (the same key you used in dependencies). Each entry configures class-level inclusion into the final jar.
"shading": { "adventure-api": { "include": ["net/kyori/adventure/api/**"], "exclude": ["net/kyori/adventure/api/internal/**"] }}- Omitting
includeis the same as["**"](everything). excludeis subtracted afterincludematches.- Patterns are forward-slashed jar-entry paths.
*matches one segment.**matches any depth, including zero segments. - Dependencies without a shading entry are not shaded. They’re still on the compile classpath, but don’t end up inside your jar.
For workspace: sibling deps, shading uses the sibling’s name, and the build expects the sibling to have been built already. Running pluggy build from the repo root orders workspaces topologically. Running it from inside a workspace doesn’t, and the shade step errors with “has not been built yet, expected jar at …”.
resources (optional)
Object keyed by the output path inside the jar. Values are paths relative to the project root.
"resources": { "config.yml": "src/config.yml", "lang/": "src/lang"}Trailing / on a key means “copy the directory recursively”. Files under these extensions are run through ${project.x} substitution before landing in the jar: .yml, .yaml, .json, .properties, .txt, .md. Binary files are hardlinked if possible, copied otherwise.
A resources entry that targets the descriptor path (plugin.yml, bungee.yml, velocity-plugin.json, or META-INF/sponge_plugins.json) takes precedence over pluggy’s auto-generated descriptor. Useful when you need fields pluggy doesn’t model yet (commands:, permissions:, softdepend:, and so on).
On output-path collisions, the first-declared entry wins and subsequent ones are skipped with a warning.
jdk (optional)
Pin the JDK pluggy installs for build, test, and dev. When omitted, pluggy derives the required Java major from compatibility.versions[0] (Java 21 for 1.20.5 and later, Java 17 for 1.18 to 1.20.4, and so on) and downloads the default distribution (Temurin) on first use.
"jdk": { "major": 21, "distribution": "zulu"}| Field | Default | Notes |
|---|---|---|
major | derived | Java major release. Overrides the version-derived default. |
distribution | "temurin" | One of temurin, zulu, liberica, corretto, microsoft, graalvm_community. See pluggy sdk. |
pluggy sdk use 21 --distribution zulu writes this block for you. Pin
when your team has standardized on a non-default distribution, or when a
project must build against a specific Java major regardless of the MC
version.
dev (optional)
Knobs for pluggy dev.
"dev": { "port": 25565, "memory": "2G", "onlineMode": false, "jvmArgs": ["-XX:+UseG1GC"], "serverProperties": { "view-distance": 6 }, "extraPlugins": ["./plugins/helper.jar"]}| Field | Default | Notes |
|---|---|---|
port | 25565 | --port overrides. Written to server.properties. |
memory | "2G" | JVM heap; produces -Xmx<value>. --memory overrides. |
onlineMode | false | --offline on the command line forces false and beats the config. |
jvmArgs | [] | Inserted between -Xmx... and -jar server.jar. |
serverProperties | {} | Merged with pluggy’s defaults (motd, online-mode, server-port). User keys win on conflict. |
extraPlugins | [] | Jar paths relative to the workspace root, hardlinked into dev/plugins/ at start. |
hotswap | true | Hotswap configuration. See below. Set to false to disable. |
extraPlugins is how you inject a runtime prerequisite that isn’t in dependencies (for example a locally-patched EssentialsX).
dev.hotswap
Controls whether pluggy dev swaps changed classes in place via JetBrains Runtime + HotswapAgent. Default behaviour is on, which is equivalent to "hotswap": true.
Three accepted shapes:
true(default): hotswap on with default settings (JetBrains Runtime as the JDK, fall back to/reloadwhen a change can’t be hotswapped).false: disable hotswap entirely.pluggy devfalls back to/reload(with--reload) or full restart.- An object with these keys:
| Field | Default | Notes |
|---|---|---|
jdk | "jbr" | "jbr" downloads JetBrains Runtime to the cache. "system" uses java from PATH instead. |
fallback | "reload" | Action when a class change can’t be hotswapped. "reload" sends /reload. "restart" restarts. |
--no-hotswap on the CLI overrides this block to false. See Dev server for what hotswap does at runtime.
workspaces (optional)
Array of paths (relative or absolute, forward-slashed) to sibling project.json files. Each entry must point at a directory that contains a project.json. See Workspaces.
A project that declares workspaces is a root. It doesn’t have to declare main, it doesn’t build a jar itself, and its compatibility, authors, description, and registries are inherited by workspaces that don’t declare their own.
Template variables
Several fields are substituted into files at build time. Syntax is ${dotted.key}. Every scalar on the project object is available.
| Variable | Value |
|---|---|
${project.name} | name |
${project.version} | version |
${project.description} | description |
${project.main} | main |
${project.className} | Last segment of main |
${project.packageName} | Everything in main before the last segment |
${project.compatibility.versions.0} | First entry of the versions array |
Arrays expand to numerically-suffixed keys (${project.authors.0}, ${project.authors.1}, and so on).
Substitution runs on resources with the allowlisted extensions above, and on the src/config.yml and main-class templates produced by init.
Validation
The primary validator is pluggy doctor, which checks:
namematches^[a-zA-Z0-9_]+$.versionmatches^\d+\.\d+\.\d+(-[a-zA-Z0-9]+)?$.compatibility.versionsandcompatibility.platformsare non-empty arrays.- Every entry in
compatibility.platformsis a registered platform id. - All workspaces share a descriptor family when they share a primary platform.
- Workspaces form a DAG (no cycles via
workspace:deps).
Errors that would prevent a build show up here. Warnings are advisory (for example a JDK outside the 8 to 21 band when the primary platform is spigot or bukkit).
What pluggy does not read
package.json,pom.xml,build.gradle*: ignored even if present..classpath,.project,.idea/: pluggy writes these automatically (see IDE integration) but never reads them.pluggy.lockis notproject.json. It’s a separate file produced byinstallandbuild, documented in Dependencies.