Skip to content

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:

  1. The descriptor’s main field.
  2. ${project.className} and ${project.packageName} template substitution in resource files and the initial Java class.
  3. The output directory layout inside src/ during init (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 legacy 1.21.8 shape 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-version in the Bukkit descriptor, and the JDK picker for IntelliJ. For velocity and sponge, this still reads as a Minecraft version. pluggy resolves the actual velocity-api or spongeapi Maven 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 (same plugin.yml, bungee.yml, velocity-plugin.json, or META-INF/sponge_plugins.json target). Mixing families fails early with “Split them into separate workspaces, one per family.”.

The full platform roster ships with the binary:

iddescriptorMaven coordinate
paperplugin.ymlio.papermc.paper:paper-api
foliaplugin.ymldev.folia:folia-api
spigotplugin.ymlorg.spigotmc:spigot-api (SNAPSHOT)
bukkitplugin.ymlorg.spigotmc:spigot-api (SNAPSHOT)
velocityvelocity-plugin.jsoncom.velocitypowered:velocity-api
waterfallbungee.ymlio.github.waterfallmc:waterfall-api
travertinebungee.yml(no Maven API; compile against waterfall)
spongeMETA-INF/sponge_plugins.jsonorg.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:

Schemeversion meaning
modrinthExact version number as listed on Modrinth, or "*" for latest.
mavenMaven version. Soft pins (1.2.3) and lower-bounded ranges ([1.2,2.0)) work; property expansion does not.
fileArbitrary label. The content-addressed integrity hash is what actually identifies the file.
workspaceIgnored. 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 include is the same as ["**"] (everything).
  • exclude is subtracted after include matches.
  • 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"
}
FieldDefaultNotes
majorderivedJava 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"]
}
FieldDefaultNotes
port25565--port overrides. Written to server.properties.
memory"2G"JVM heap; produces -Xmx<value>. --memory overrides.
onlineModefalse--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.
hotswaptrueHotswap 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 /reload when a change can’t be hotswapped).
  • false: disable hotswap entirely. pluggy dev falls back to /reload (with --reload) or full restart.
  • An object with these keys:
FieldDefaultNotes
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.

VariableValue
${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:

  • name matches ^[a-zA-Z0-9_]+$.
  • version matches ^\d+\.\d+\.\d+(-[a-zA-Z0-9]+)?$.
  • compatibility.versions and compatibility.platforms are non-empty arrays.
  • Every entry in compatibility.platforms is 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.lock is not project.json. It’s a separate file produced by install and build, documented in Dependencies.