Maven 4: POM Model & Consumer POM
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page goes deeper than the overview in Maven 4: What’s New on the one
part of Maven 4 that touches every project: the POM. Maven 4.0.0-rc-7 is a release candidate, not a final release, and no 4.0.0 had been
published on 2026-10-16, when everything here was run: the schema, the consumer POM rules and the messages quoted below can still change.
The Maven 4 examples ran on rc-7 on JDK 26 (the whole tidepool build was run once more on JDK 17.0.14, the minimum Java for Maven 4, with the
identical consumer POM); the comparisons ran on Maven 3.10.0, the current Maven 3 release, and Maven 3.9.16. Plugin versions are the stable releases of
2026-10-16, except the Maven 4 generation of the compiler and resources plugins (4.0.0-beta-5, 4.0.0-beta-2), which the sources section needs. Each
example lives in its own scratch directory with its own local repository; paths in console output were shortened to /work/..., the lines
Loaded N auto-discovered prefixes ... that rc-7 prints on the first request to each repository were removed, and any other trimming is marked.
The overview already covers the reasons (the big idea),
the one-line description of each feature, mvnup and the Maven 3 error you get from a 4.1.0 POM. This page covers what the overview leaves out:
the schema difference element by element, the exact files Maven 4 writes, three ways to publish a consumer POM, and a set of traps measured on rc-7.
For the older mechanics see POM Fundamentals,
Inheritance, Aggregation & the Effective POM and
Maven CI-Friendly Versions; the lifecycle changes are on
Maven 4: Lifecycle and the upgrade procedure on
Migrating to Maven 4.
Three POMs, not one
Maven 3 has two POM-like things: the file in your source tree and the effective POM Maven computes from it (inheritance, interpolation, profiles). The published file was just a copy of the first. Maven 4 adds a third and publishes that instead:
| Name | What it is | Seen by |
|---|---|---|
Build POM |
The |
You, Maven 4 and your IDE |
Effective POM |
The in-memory model after inheritance, interpolation and profile activation. |
The build itself |
Consumer POM |
Generated at |
Everyone who depends on the artifact: Maven 3, Maven 4, Gradle, scanners, Maven Central |
The diagram is a summary of what the runs below show, not a description of Maven’s internal classes. Everything on the left of install and deploy is your
business; everything on the right is a contract with people you will never meet, which is why the schema they read could not change.
Model version 4.1.0
A POM is a model 4.1.0 POM when the root element is in the namespace http://maven.apache.org/POM/4.1.0. The conventional header, which is what the
Maven Upgrade Tool writes, is:
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 https://maven.apache.org/xsd/maven-4.1.0.xsd">
The schema URL is a redirect that follows the current release of Maven 4. On 2026-10-16 it answered:
$ curl -sIL https://maven.apache.org/xsd/maven-4.1.0.xsd | grep -iE "^(HTTP|location)"
HTTP/2 302
location: https://maven.apache.org/xsd/maven-4.1.0-rc-6.xsd
HTTP/2 200
(The target still carried the name of release candidate 6 while rc-7 was current, so it moves between releases: use maven-4.1.0.xsd in your POMs, not the target of the
redirect.) The file is for IDEs and schema-aware editors.
The element modelVersion becomes optional
In a 4.1.0 POM the <modelVersion> element can be left out, because the namespace already says it. The upgrade tool removes it. The consequence is easy to
miss on a team that still has a Maven 3 user: Maven 3 does not know the new namespace, finds no modelVersion, and reports that, not the real cause. This is
the same tidepool parent POM with the root attribute taken out, built with Maven 3.10.0:
$ mvn -q validate
[ERROR] [ERROR] Some problems were encountered while processing the POMs:
[ERROR] 'modelVersion' is missing. @ io.tidepool:tidepool-parent:${revision}, /work/noroot/pom.xml, line 4, column 110
[ERROR] 'modelVersion' is missing. @ io.tidepool:tidepool-parent:${revision}, /work/noroot/pom.xml, line 4, column 110
@
With the root="true" attribute present the first message is the one the overview shows, Unknown attribute 'root' for tag 'project', and Maven 3.9.16 prints the
same lines. Either way the rule is the one the overview gives: a repository with a 4.1.0 build POM can only be built with Maven 4.
What the schema adds, deprecates and removes
Comparing maven-4.0.0.xsd with the 4.1.0 schema that the redirect above points to, type by type, for every element and attribute name that appears or disappears, gives
the complete list below. No other element or attribute name differs, so dependencyManagement, profiles, repositories or distributionManagement, for example, are
unchanged.
| Where | New in 4.1.0 | Notes |
|---|---|---|
|
|
|
|
|
Replaces |
|
|
Replaces |
|
|
|
|
|
Lets a build extension take configuration. |
Two further 4.1.0 features are behaviour, not new names: the bom packaging (build POM only; the consumer POM says pom) and the use of <classifier> and <exclusions> on an import-scope
BOM dependency, both described in the overview and in Dependency Management & BOMs.
Two elements disappear from the schema: <reports> (in <project> and in <profile>, long deprecated in favour of <reporting>) and <goals> inside <plugin>.
A POM that still uses them is no longer valid 4.1.0, which is one more reason to move to 4.1.0 deliberately and not by search and replace. The
<source> element is a whole new type; its fields are in Declaring sources.
Converting tidepool
The conversion of the tidepool project is mostly mechanical, so it is a good fit for mvnup. The starting point is a Maven 3 style 4.0.0 build with a ${revision}
version, three <module> entries and repeated parent and dependency coordinates. It builds on Maven 3.10.0 and on Maven 4.0.0-rc-7 without change; on rc-7 it only
adds the warning that no root directory was found (The root directory, in practice). mvnup apply with --model-version 4.1.0 --infer, in a scratch git repository, reported:
$ mvnup -d . apply --model-version 4.1.0 --infer -B
[INFO] Executing 3 upgrade strategy(ies):
[INFO] - Upgrading POM model version
[INFO] - Applying Maven inference optimizations
[INFO] - Migrating source configuration to <source> elements
...
[INFO] • Updated modelVersion to 4.1.0
[INFO] • Updated namespace to http://maven.apache.org/POM/4.1.0
[INFO] • Updated xsi:schemaLocation
[INFO] • Converted <modules> to <subprojects>
[INFO] • Converted 3 <module> elements to <subproject>
...
[INFO] • Removed: redundant subprojects list (matches direct children)
[INFO] • Removed: modelVersion element (can be inferred from namespace)
...
[INFO] • Removed: parent groupId (child has no explicit groupId)
[INFO] • Removed: parent version (child has no explicit version)
[INFO] • Removed: parent artifactId (can be inferred from relativePath)
...
[INFO] • Migrated property: maven.compiler.release = 17
[INFO] • Set targetVersion: 17
...
[INFO] Creating .mvn directory if needed to avoid root directory warnings...
(Only the headline lines of each strategy are shown.) Three details are worth knowing. The tool deletes the list of subprojects when it equals the direct
children, trusting discovery (Subprojects in detail). It does not touch a dependency such as <version>${project.version}</version>, so the version is still repeated
in the POMs of tidepool-engine and tidepool-cli. And it moves maven.compiler.release into <sources>, which broke the build with the 3.x compiler plugin,
a trap that the overview already measured (the compiler failed with records are not supported in -source 8, because 3.x ignores targetVersion); with the 4.x
compiler plugin (4.0.0-beta-5) the same converted project compiled for release 17, and the class file major version was 61. On a copy without a .mvn directory the tool also printed Created .mvn directory (an empty one), so it silences the missing-root warning for you. I finished the conversion by
hand: I restored the property, removed the dependency versions and used the root attribute instead of the empty directory. The result, which the rest of the page uses, is:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 https://maven.apache.org/xsd/maven-4.1.0.xsd"
root="true">
<groupId>io.tidepool</groupId>
<artifactId>tidepool-parent</artifactId>
<version>${revision}</version>
<packaging>pom</packaging>
<properties>
<revision>1.0.0-SNAPSHOT</revision>
<maven.compiler.release>17</maven.compiler.release>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<!-- build/pluginManagement with the plugin versions pinned (compiler 3.16.0, jar 3.5.1, ...) -->
</project>
and each subproject, for example tidepool-engine, is eleven lines with no coordinates but its own artifact id:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.1.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 https://maven.apache.org/xsd/maven-4.1.0.xsd">
<parent/>
<artifactId>tidepool-engine</artifactId>
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-model</artifactId>
</dependency>
</dependencies>
</project>
Subprojects in detail
<subprojects> is <modules> renamed, with the same meaning: a list of relative paths to directories. The overview shows discovery; the rules that remain are
these, all run on rc-7.
The old element still works, with a warning. A 4.1.0 POM that keeps <modules> builds, and the warning only shows its text with -e:
$ mvn -e validate
[INFO] Error stacktraces are turned on.
[INFO] Scanning for projects...
[WARNING]
[WARNING] 1 problem was encountered while building the effective model for 'io.tidepool:tidepool-parent:1.0.0-SNAPSHOT' (use -e to see details)
[WARNING] 'modules' deprecated modules element, use subprojects instead
[WARNING]
...
Both elements in one POM is an error, and so is the new element in an old POM. With <modules> and <subprojects> together in a 4.1.0 POM:
[ERROR] Encountered 1 problem(s) while processing the POMs
[ERROR] The build could not read 1 project -> [Help 1]
[ERROR]
[ERROR] The project io.tidepool:tidepool-parent:pom:1.0.0-SNAPSHOT (/work/mix/pom.xml) has 1 error
[ERROR] 'modules' cannot use both modules and subprojects element @ io.tidepool:tidepool-parent:1.0.0-SNAPSHOT, /work/mix/pom.xml
and with <subprojects> in a POM whose namespace is still 4.0.0:
[ERROR] The project io.tidepool:tidepool-parent:pom:1.0.0-SNAPSHOT (/work/sub40/pom.xml) has 1 error
[ERROR] 'subprojects' unexpected subprojects element @ io.tidepool:tidepool-parent:1.0.0-SNAPSHOT, /work/sub40/pom.xml
Discovery has three conditions. The official rule is: pom packaging, no <subprojects> or <modules> element, and a pom.xml in a direct subdirectory. Write the list
when you want only some of the directories. The ordering rules (dependencies, plugins, extensions and then the declared order) are in the Maven 4 multi-project guide and in
Multi-Module Builds & the Reactor.
A single subproject can be built from its own directory or with -f. Maven walks up to find the root (a .mvn directory or a POM with root="true"), so
the dependency on tidepool-model, which has no version in the engine’s POM, can be resolved wherever you start. Both of these succeeded, with the model already installed in the
local repository:
$ mvn -f tidepool-engine/pom.xml -q validate
$ mvn -pl tidepool-engine validate
[INFO] Building tidepool-engine 1.0.0-SNAPSHOT
[INFO] BUILD SUCCESS
(The first command prints nothing on success because of -q.) Without the parent on disk, the shorthand fails with a message about the parent, not about the inference: a
copy of the engine POM placed in a lone directory gave
[ERROR] The project (/work/orphan/pom.xml) has 3 errors
[ERROR] 'parent.groupId' is missing. @ [unknown-group-id]:tidepool-engine:[unknown-version], /work/orphan/pom.xml, line 5, column 3
[ERROR] 'parent.artifactId' is missing. @ [unknown-group-id]:tidepool-engine:[unknown-version], /work/orphan/pom.xml, line 5, column 3
[ERROR] Failed to load project /work/orphan/pom.xml: 2 problems were encountered while building the effective model
(Three of the lines were removed.) The practical consequence: a subproject POM copied out of its tree, into an archetype or an email, no longer describes itself.
The root directory, in practice
The overview lists the attribute and the three properties. Here is what they evaluate to. Three runs of the Maven Help Plugin’s evaluate goal gave the values below: inside tidepool-cli, from the root with -f tidepool-cli/pom.xml, and at the root with -N. The plugin
wraps path values in a <path> element and, on JDK 26, prints four warnings about sun.misc.Unsafe from a library it uses; both were removed from the table.
| Expression | cd tidepool-cli |
-f tidepool-cli/pom.xml from the root |
At the root with -N |
|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
The command for each cell was mvn org.apache.maven.plugins:maven-help-plugin:3.5.2:evaluate -Dexpression=NAME -q -DforceStdout. The root properties are the same wherever you
start; the top directory follows the POM you started from. Use ${session.rootDirectory} or ${project.rootDirectory} for files shared by the whole tree (a Checkstyle
suppression file, a license header) and ${basedir} for files of one subproject. The overview’s table says the root properties are not always set: define a root in every tree that uses them.
Maven creates the directory for you, once. A 4.0.0 build with no .mvn and no attribute prints Unable to find the root directory ... on the first Maven 4 run, and that run itself creates
.mvn/target/project-local-repo, so the second run finds a .mvn directory and the warning is gone:
$ mvn verify # first run
[WARNING] Unable to find the root directory. Create a .mvn directory in the root directory or add the root="true" attribute on the root project's model to identify it.
[INFO] BUILD SUCCESS
$ mvn verify # second run, same directory
[INFO] BUILD SUCCESS
(Only the warning and result lines are shown.) Do not count on that: the directory is a build artifact, and a fresh clone warns again.
A side effect to put in .gitignore. Once a root is known, Maven 4 keeps .mvn/target/project-local-repo in it, even for mvn verify with no install, and puts there, per project, the
jar, a copy of the build POM as name-version.pom and the generated name-version-consumer.pom; it looks like the area where the reactor stages the consumer form of each project.
mvn clean does not remove it, because the directory is outside the subproject’s target:
.mvn/target/project-local-repo/io.tidepool/tidepool-cli/1.0.0-SNAPSHOT/tidepool-cli-1.0.0-SNAPSHOT-consumer.pom
.mvn/target/project-local-repo/io.tidepool/tidepool-cli/1.0.0-SNAPSHOT/tidepool-cli-1.0.0-SNAPSHOT.pom
.mvn/target/project-local-repo/io.tidepool/tidepool-cli/1.0.0-SNAPSHOT/tidepool-cli-1.0.0-SNAPSHOT.jar
(One of the four projects is shown.) Add .mvn/target/ to the ignore file next to target/; the other files that may live in .mvn (maven.config, jvm.config,
extensions.xml, maven-user.properties) are meant to be committed.
Inference rules
Parent inference and version inference only exist in a model 4.1.0 POM and are applied while the POM is read, before the consumer POM is generated. Observed on rc-7:
-
An empty
<parent/>means<relativePath>..</relativePath>. A<parent>with only a<relativePath>takes group, artifact and version from thepom.xmlfound there. -
<groupId>and<version>of a subproject default to the parent’s, as before; with inference you also leave out the parent’s coordinates. -
A dependency whose group and artifact id name another project of the same build needs no
<version>. It is the version of that project, which is why the engine can list the model without one. -
Inference does not touch a version such as
${project.version}that you typed: Maven 4 builds a 4.0.0 project with that expression and publishes it unchanged. In the 4.0.0tidepool-enginethe consumer POM (built by rc-7) still contains<version>${project.version}</version>in the dependency, but the parent version was resolved to1.0.0-SNAPSHOTand the<relativePath>removed. The expression is correct for consumers because it is evaluated against the project’s own version.
Inference is a convenience of the build POM. The consumer POM that is published always spells the coordinates out, so a Maven 3 or Gradle user never has to implement it.
Declaring sources
The overview shows two source roots and the 4.x compiler plugin’s need for a root directory. The <source> element has more fields. They come from the 4.1.0 schema, so a
plugin must read them to honour them ([sources-plugin-support]):
| Element | Meaning |
|---|---|
|
Path relative to the POM. Repeating |
|
|
|
|
|
The Java module a directory belongs to, for projects with several modules in one build. |
|
The Java release ( |
|
Output directory; the default is |
|
|
|
|
|
Ant-style patterns. |
A scratch project with the four kinds of source (a main Java directory, an optional extra one, a filtered resources directory and a test directory), built with clean test on rc-7
with the compiler plugin 4.0.0-beta-5 and the resources plugin 4.0.0-beta-2:
<build>
<sources>
<source>
<scope>main</scope>
<lang>java</lang>
<directory>src/main/java</directory>
</source>
<source>
<scope>main</scope>
<lang>java</lang>
<directory>src/extra/java</directory>
<enabled>true</enabled>
</source>
<source>
<scope>main</scope>
<lang>resources</lang>
<directory>src/main/conf</directory>
<stringFiltering>true</stringFiltering>
</source>
<source>
<scope>test</scope>
<lang>java</lang>
<directory>src/test/java</directory>
</source>
</sources>
</build>
The class files and resources that ended in target, and the filtered file (the source line is app.name=${project.artifactId}):
classes/app.properties
classes/p/A.class
classes/p/Extra.class
test-classes/p/ATest.class
$ cat target/classes/app.properties
app.name=srcs
With <enabled>false</enabled> the same build produced no Extra.class; that is the field to use for an optional directory.
The effective POM shows how Maven 4 keeps the old API working for plugins that do not know <sources>: the legacy elements are derived from the new ones, and defaults
(main, java) are omitted. This is help:effective-pom for that project, excerpted:
<sources>
<source>
<directory>/work/srcs/src/main/java</directory>
</source>
<source>
<lang>resources</lang>
<stringFiltering>true</stringFiltering>
<directory>/work/srcs/src/main/conf</directory>
</source>
<source>
<scope>test</scope>
<directory>/work/srcs/src/test/java</directory>
</source>
</sources>
<sourceDirectory>/work/srcs/src/main/java</sourceDirectory>
<testSourceDirectory>/work/srcs/src/test/java</testSourceDirectory>
<resources>
<resource>
<filtering>true</filtering>
<directory>/work/srcs/src/main/conf</directory>
</resource>
</resources>
(The extra directory, scriptSourceDirectory and testResources entries were removed for brevity.) This is also why the 3.x resources plugin 3.5.0 copied and
filtered src/main/conf correctly in a run on rc-7: it reads <resources>. The 3.x compiler plugin line compiled only src/main/java, the single legacy source directory.
Three traps measured on rc-7:
-
<enabled>is not interpolated. With<enabled>${with.extra}</enabled>and<with.extra>true</with.extra>in the properties, the effective POM printed<enabled>false</enabled>for the extra directory andExtra.classwas missing, both with the property in the POM and with-Dwith.extra=true. Write the literal value; to switch a directory with a profile, put the<source>inside the profile. -
The consumer POM loses the whole
<build>. When the build POM has<sources>, the 4.0.0 consumer POM written by rc-7 has no<build>element at all, plugins included; the same project without<sources>keeps its plugin list. Nothing a consumer needs is lost, but do not rely on plugin information in a published POM (Default: the build POM, resolved). -
Plugin support is uneven. The overview names the compiler, install, deploy and resources lines that use the new API. A plugin that reads only the legacy elements keeps working because of the derivation above, and ignores
targetVersion,moduleandtargetPath.
The consumer POM, measured
Everything in this section was produced by installing the example into an empty local repository and comparing files. The project is a parent with a 4.1.0 child that has one dependency of each scope, a property, a BOM import in the parent and a plugin configuration:
<project xmlns="http://maven.apache.org/POM/4.1.0" ...>
<parent/>
<artifactId>flat-app</artifactId>
<name>Flat App</name>
<properties>
<only.in.build.pom>yes</only.in.build.pom>
</properties>
<dependencies>
<dependency> <!-- version from the parent's dependencyManagement -->
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency> <!-- runtime -->
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.17</version>
<scope>runtime</scope>
</dependency>
<dependency> <!-- provided -->
<groupId>jakarta.servlet</groupId>
<artifactId>jakarta.servlet-api</artifactId>
<version>6.1.0</version>
<scope>provided</scope>
</dependency>
<dependency> <!-- test, version from the JUnit BOM imported in the parent -->
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive><manifestEntries><Built-By-Pom>flat-app</Built-By-Pom></manifestEntries></archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
(The parent is a pom project with root="true", the version 1.0.0, a commons.version property, the JUnit BOM import and pinned plugin versions; the
xmlns header lines were shortened.) This POM is this page’s own lab setup, not an example from the Maven documentation.
Default: the build POM, resolved
mvn install installed flat-app-1.0.0.jar, flat-app-1.0.0.pom and flat-app-1.0.0-build.pom. The default consumer POM is not stripped of build information in rc-7. This
is the file, with the unchanged <dependencies> and <build> left out:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.tidepool</groupId>
<artifactId>flat-parent</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>flat-app</artifactId>
<name>Flat App</name>
<description>Shows what the consumer POM keeps and drops.</description>
<properties>
<only.in.build.pom>yes</only.in.build.pom>
</properties>
<dependencies>
...
</dependencies>
<build>
...
</build>
</project>
What changed compared with the build POM is only the form: the model version is 4.0.0, the <parent/> shorthand is a real parent, <relativePath> is gone, and everything else, the
property, the four dependencies with their scopes (the managed versions are still omitted: the parent supplies them), and the jar plugin configuration, was copied. The file is also reformatted (the source layout is not kept). The parent is installed too, because the consumer POM refers to it. The official comparison table says that plugin configuration and properties exist only in the
build POM and that the consumer POM "might" lose more build data in future; on rc-7 they were kept, so treat the published POM as containing build information and avoid putting anything
sensitive in the build section.
A pom-packaging parent is treated the same way: installing tidepool-parent kept <properties> and <pluginManagement>, which children need, dropped the <modules> list (and the root attribute
of the 4.1.0 form) and replaced ${revision} by its value.
Flattened: the minimal consumer POM
Set maven.consumer.pom.flatten to true and the consumer POM becomes self-contained. This is the same build with -Dmaven.consumer.pom.flatten=true:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>io.tidepool</groupId>
<artifactId>flat-app</artifactId>
<version>1.0.0</version>
<name>Flat App</name>
<description>Shows what the consumer POM keeps and drops.</description>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.20.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>2.0.17</version>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
| Part of the project | Build POM | Default consumer POM | Flattened consumer POM |
|---|---|---|---|
Model version |
|
|
|
|
|
Kept, version filled in |
Removed; |
Properties |
Yes |
Yes |
No |
Plugins and their configuration |
Yes |
Yes (unless the build POM has |
No |
|
Yes |
Yes, managed versions omitted |
Yes, with the managed version and the |
|
Yes |
Yes |
No |
|
Yes |
Yes |
Yes |
The dropped scopes (provided and test) are not transitive anyway, so what flattening mainly removes is the need to resolve the parent and the imported BOM to read the dependencies. The article gives the reason it is off by default: to avoid unexpected behaviour and to keep control of how (transitive) dependencies are listed. The parent POM of a flattened
child is still installed, since it is a normal project of the build.
Switching it on: command line or .mvn/maven-user.properties
The property is a user property, so -D works. The official article names a second place, which is the one to prefer in a repository because it applies to every contributor and to CI, with no
change to the command line. Putting a single line in .mvn/maven-user.properties:
maven.consumer.pom.flatten=true
and running a plain mvn clean install gave a consumer POM with no <parent> and only the compile and runtime scopes, exactly like the -D run. It also took effect when the build was started
from another directory with -f flat2/app/pom.xml: that consumer POM again had no <parent> and no <properties>. I found no pom.xml element that does the same; the switch is a property, because it is
about how you publish, not about what the project is.
The 4.1.0 consumer POM and preserve.model.version
The consumer POM is 4.0.0 only as long as everything in the build POM can be written in 4.0.0. A condition-based profile cannot. A scratch project with this profile (the build POM has no other 4.1.0 feature):
<profiles>
<profile>
<id>jdk-17-plus</id>
<activation>
<condition>inrange(${java.specification.version}, "[17,)")</condition>
</activation>
<dependencies>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.20.0</version>
</dependency>
</dependencies>
</profile>
</profiles>
produced a consumer POM with <modelVersion>4.1.0</modelVersion> and the profile untouched. The overview says that Maven warns when this happens; on rc-7 I saw no warning in the build output,
only the Installing .../consumer-NNN.pom line, so check the installed file. A downstream project whose only dependency is this artifact (version 1.0.0) shows why it matters, because commons-lang3 lives only in the
profile. The dependency tree, on Maven 3.10.0 and on Maven 4.0.0-rc-7:
$ mvn org.apache.maven.plugins:maven-dependency-plugin:3.11.0:tree # Maven 3.10.0
[INFO] io.example:downstream:jar:1.0
[INFO] \- io.tidepool:cp:jar:1.0.0:compile
$ mvn org.apache.maven.plugins:maven-dependency-plugin:3.11.0:tree # Maven 4.0.0-rc-7
[INFO] io.example:downstream:jar:1.0
[INFO] \- io.tidepool:cp:jar:1.0.0:compile
[INFO] \- org.apache.commons:commons-lang3:jar:3.20.0:compile
Maven 3.10.0 and 3.9.16 both read the 4.1.0 consumer POM without a message and silently drop the profile and with it the dependency. A library that publishes for Maven 3 users must therefore avoid
<condition>, or use the classic <jdk> activation, which 4.0.0 can express.
The preserve.model.version attribute decides whether the model version of the build POM is passed on. On the project without the profile, preserve.model.version="false" (the default) produced a 4.0.0 consumer POM, and
preserve.model.version="true" produced one with <modelVersion>4.1.0</modelVersion> and the 4.1.0 namespace. In both cases name-1.0.0-build.pom was the 4.1.0 source file, with the attribute itself kept. Use true only when
you want the published POM to be 4.1.0; as the dependency trees above show, Maven 3 still reads such a file, but it ignores what it does not know.
What Maven 3 consumers see
The point of the whole mechanism is that a project built with Maven 4 stays usable from Maven 3. A downstream Maven 3.10.0 project whose only dependency is tidepool-cli, installed by rc-7 from the 4.1.0 tidepool build:
$ mvn org.apache.maven.plugins:maven-dependency-plugin:3.11.0:tree
[INFO] io.example:downstream:jar:1.0
[INFO] \- io.tidepool:tidepool-cli:jar:1.0.0-SNAPSHOT:compile
[INFO] \- io.tidepool:tidepool-engine:jar:1.0.0-SNAPSHOT:compile
[INFO] \- io.tidepool:tidepool-model:jar:1.0.0-SNAPSHOT:compile
The consumer POM of tidepool-cli, 16 lines against the 13 of the build POM and the 248 of help:effective-pom for the same project, is:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-parent</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<artifactId>tidepool-cli</artifactId>
<dependencies>
<dependency>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-engine</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
(The effective POM is much longer because it carries the inherited properties, the 15 plugins from the lifecycle bindings and from pluginManagement, and the Super POM defaults; it exists only in memory.)
Install and deploy write both files
The official article says Maven 4 "does not deploy the pom.xml used to build the project". On rc-7 it deploys both. Deploying the 4.1.0 tidepool build to a file: repository with
-DaltDeploymentRepository=local::file:/work/repo (the deploy plugin 3.2.0, as pinned in the parent; the real remote was not used) produced, for tidepool-cli:
io/tidepool/tidepool-cli/1.0.0-SNAPSHOT/tidepool-cli-1.0.0-20261016.145311-1-build.pom
io/tidepool/tidepool-cli/1.0.0-SNAPSHOT/tidepool-cli-1.0.0-20261016.145311-1.jar
io/tidepool/tidepool-cli/1.0.0-SNAPSHOT/tidepool-cli-1.0.0-20261016.145311-1.pom
io/tidepool/tidepool-cli/1.0.0-SNAPSHOT/maven-metadata.xml
io/tidepool/tidepool-cli/maven-metadata.xml
...
(The .md5 and .sha1 files of each entry were left out.) So the build POM, with the build classifier, is published next to the consumer POM and a repository manager
sees an artifact it has not seen before. Plan for it in the policies of your repository manager (see
Install, Deploy & Distribution Management and
Repositories, Mirrors & Repository Managers). Publishing to Maven Central was not run here; its
validation of the POM is on Publishing to Maven Central.
CI-friendly versions and the consumer POM
This is the case the consumer POM was built for. The tidepool build in both forms, built with -Drevision=2.0.0 and no flatten plugin: Maven 3.10.0 installed the child with the unresolved placeholder in its parent,
Maven 4.0.0-rc-7 installed the resolved coordinates, including those of the dependency:
# Maven 3.10.0, the 4.0.0 tidepool build
<parent>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-parent</artifactId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
# Maven 4.0.0-rc-7, the 4.1.0 tidepool build (tidepool-cli consumer POM)
<parent>
<groupId>io.tidepool</groupId>
<artifactId>tidepool-parent</artifactId>
<version>2.0.0</version>
</parent>
...
<artifactId>tidepool-engine</artifactId>
<version>2.0.0</version>
On rc-7 the installed parent POM also had <revision>2.0.0</revision> in its properties. The full recipe,
including changelist, sha1 and the pipelines that set them, is on Maven CI-Friendly Versions.
Checklist before moving a project to 4.1.0
-
Every contributor and CI image can run Maven 4 (Java 17 to run it). A 4.1.0 build POM cannot be read by Maven 3, and the first message it prints is about
rootormodelVersion, not about the version. -
A root exists:
root="true"in the top POM, or a.mvndirectory. The 4.x compiler plugin needs it, the directory properties need it, and Maven warns without it. -
mvnup applywas reviewed in a diff:maven.compiler.releasemoved into<sources>(broken for the 3.x compiler plugin), subproject lists were deleted,${project.version}dependencies were left. -
No
<reports>and no<goals>inside<plugin>, which 4.1.0 no longer defines. -
No
<condition>profile in a library with Maven 3 users, or you accept that they silently lose its dependencies. -
The published consumer POM was inspected (
~/.m2or afile:repository) once per release candidate of Maven 4: the rules above are rc-7 behaviour, and the flatten default and the-build.pomclassifier are the likeliest to change. -
.mvn/target/is ignored by source control.
Equivalents in Ant and Gradle
Gradle already separates the build from what consumers read. The build script is never published; a publication generates a POM (and an Ivy descriptor if asked) from a model you configure, and by
default Gradle publishes its own metadata file next to it, which Gradle consumers prefer. The Maven 4 consumer POM brings Maven to the same position, with the difference that the build POM is still
deployed. See Publishing for Gradle and
Repositories & Publishing across Tools for the side-by-side. Apache Ant has no project model, so there is no
build-versus-consumer split to compare; Ivy writes an ivy.xml that is the only descriptor, see Comparing Ant, Maven & Gradle.
Moving a project between the tools is covered on Migrating between Tools.