Skip to content

Fix #926: Apply enum defaults for types already on the classpath#1779

Open
manerow wants to merge 2 commits intojoelittlejohn:masterfrom
manerow:fix/926-enum-default-classpath-types
Open

Fix #926: Apply enum defaults for types already on the classpath#1779
manerow wants to merge 2 commits intojoelittlejohn:masterfrom
manerow:fix/926-enum-default-classpath-types

Conversation

@manerow
Copy link
Copy Markdown

@manerow manerow commented Feb 12, 2026

Problem

When an enum type already exists on the plugin classpath (e.g. from a dependency JAR), DefaultRule.getDefaultValue() fails to generate the fromValue(...) initialiser.

The root cause is that EnumRule returns a JDirectClass (a reference to the pre-existing classpath class) instead of a JDefinedClass (a freshly generated class). DefaultRule only checks fieldType instanceof JDefinedClass when deciding whether to apply enum defaults, so classpath enum types fall through to return JExpr._null().

This means any project that depends on a JAR containing pre-compiled enum classes while regenerating POJOs from the same schemas will silently lose all enum default values.

Example

Given a schema with:

"provider": {
  "javaType": "com.example.ProviderType",
  "type": "string",
  "enum": ["system", "user", "automation"],
  "default": "user"
}

Expected (and what you get when the enum is generated from scratch):

private ProviderType provider = ProviderType.fromValue("user");

Actual (when ProviderType.class already exists on the classpath):

private ProviderType provider = null;

Fix

Add a fallback branch in DefaultRule.getDefaultValue() that detects classpath enum types using reflection:

  1. isClasspathEnum() — checks if a non-JDefinedClass type is an enum on the classpath via Class.isEnum()
  2. getDefaultEnumFromClasspath() — finds the fromValue method via reflection, determines the backing type from its parameter, and generates the correct fromValue(...) expression

This handles string-backed, integer-backed, and number-backed enums correctly by inspecting the fromValue method's parameter type.

Test

Added an integration test (DefaultIT.classpathEnumPropertyHasCorrectDefaultValue) that:

  1. Defines a pre-compiled enum class (ExistingClasspathEnum) in the test sources — this class is on the classpath during code generation, triggering the ClassAlreadyExistsException path in EnumRule
  2. Uses a schema that references this enum with "javaType" and declares "default": "beta"
  3. Generates and compiles the schema, then verifies the field is initialised to ExistingClasspathEnum.BETA instead of null

All 596 tests pass. The only pre-existing failure (DynamicPropertiesIT.shouldSetStringFieldJava7) targets Java 7 on JDK 17 and is unrelated.

Fixes #926

@joelittlejohn
Copy link
Copy Markdown
Owner

I haven't checked yet, but I'm slightly confused on this one. You should be using 'existingJavaType' to refer to something from the classpath. 'javaType' is used when you are generating a new type, but you want to specify the fully-qualified type name. No?

@manerow
Copy link
Copy Markdown
Author

manerow commented Feb 12, 2026

Thanks for the quick look @joelittlejohn!

You're right that existingJavaType is the intended way to reference a classpath type. However, the library already supports the case where javaType points to a type that happens to exist on the classpath: EnumRule catches ClassAlreadyExistsException and gracefully reuses the existing class. This works correctly for type resolution. The only gap is that DefaultRule doesn't recognize the resulting type as an enum (it checks instanceof JDefinedClass, but the reused class is a JDirectClass), so the default value is silently dropped to null.

This matters in multi-module projects where the same schemas are processed in different build contexts. The upstream module generates everything from scratch (javaType is correct there). A downstream module has its own schemas that $ref upstream definitions, so the upstream schema files must be present in the source directory for reference resolution. The plugin then processes all schemas in that directory, encountering enum types that already exist on the classpath from the upstream JAR. The downstream module doesn't control the upstream schemas and can't change javaType to existingJavaType.

This PR just completes what ClassAlreadyExistsException handling already does: it extends classpath-enum support to default values.

For the test, I used javaType because that's the real-world scenario. existingJavaType goes through TypeRule and bypasses enum generation entirely, so it wouldn't exercise this code path.

@joelittlejohn
Copy link
Copy Markdown
Owner

Fair enough. Hyrum's Law in action I suppose!

I think this is a hangover from before we separated javaType and existingJavaType. I think we should fix this for existingJavaType because it gives you a way to refer to an existing enum and choose a default value, without having to use the javaType trick.

@manerow
Copy link
Copy Markdown
Author

manerow commented Feb 13, 2026

Good point @joelittlejohn. The fix already covers existingJavaType. Both paths produce a JClass that reaches DefaultRule.getDefaultValue() the same way, and isClasspathEnum() handles both.

I've added an integration test (existingJavaTypeEnumPropertyHasCorrectDefaultValue) that uses existingJavaType with a default value and verifies it resolves correctly. All tests pass.

*
* @see <a href="https://github.com/joelittlejohn/jsonschema2pojo/issues/926">issue #926</a>
*/
private static boolean isClasspathEnum(JType fieldType) {
Copy link
Copy Markdown
Collaborator

@unkish unkish Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: why argument type is JType if invocation is done only for JClass (fieldType instanceof JClass) ?
If there's already logic to check for JDefinedClass inside, would it make sense to move fieldType instanceof JClass check aslo there ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The instanceof JClass check at the call site (line 161) serves a dual purpose: it gates the isClasspathEnum call and ensures the cast to JClass on line 163 is safe for getDefaultEnumFromClasspath.

isClasspathEnum takes JType to be self-contained. It needs its own JDefinedClass guard because JDefinedClass extends JClass, so a JDefinedClass would pass the instanceof JClass check at the call site. In practice this branch is already handled by line 157, but it keeps the method safe to call independently.

That said, I could narrow the parameter to JClass since the JDefinedClass guard still works the same way. Happy to change it if you prefer.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@unkish gentle bump on this thread 😄

@manerow manerow requested a review from unkish February 16, 2026 09:29
…lasspath

When an enum type already exists on the plugin classpath (e.g. from a
dependency JAR), EnumRule returns a JDirectClass/JReferencedClass instead
of a JDefinedClass. DefaultRule.getDefaultValue() only checked for
JDefinedClass when applying enum defaults, causing the default to be
silently replaced with null.

Add a fallback in getDefaultValue() that detects classpath enum types
via reflection and generates the correct fromValue(...) initialiser.
@manerow manerow force-pushed the fix/926-enum-default-classpath-types branch from af9e22a to 3e179df Compare February 16, 2026 10:49
*
* @see <a href="https://github.com/joelittlejohn/jsonschema2pojo/issues/926">issue #926</a>
*/
public enum ExistingClasspathEnum {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would rather move that file away from org.jsonschema2pojo.integration, maybe into com.example.classpath keeping former package name for integration tests.

* @see <a href="https://github.com/joelittlejohn/jsonschema2pojo/issues/926">issue #926</a>
*/
@Test
public void classpathEnumPropertyHasCorrectDefaultValue() throws Exception {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not quite sure as to what to think of this test - this will just cause EnumRule to return reference to existing class which should be equivalent to test that has existingJavaType.
It does no harm here, but should it be a part of DefaultIT or EnumIT, maybe someone else has also an opinion on that.


return getDefaultEnum(fieldType, value);

} else if (fieldType instanceof JClass && isClasspathEnum(fieldType)) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: instanceOf operator could be used here, eliminating the need for downstream casting

        } else if (fieldType instanceof JClass jClass && isClasspathEnum(jClass)) {

            return getDefaultEnumFromClasspath(jClass, value);

.loadClass(fieldType.fullName());
for (Method m : enumClass.getMethods()) {
if ("fromValue".equals(m.getName()) && m.getParameterCount() == 1) {
JType backingType = fieldType.owner().ref(m.getParameterTypes()[0]).unboxify();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unboxify is redundant as

  • it will be done downstream in getDefaultValue
  • .ref would throw an IllegalArgumentException for primitive type

Class<?> enumClass = Thread.currentThread().getContextClassLoader()
.loadClass(fieldType.fullName());
for (Method m : enumClass.getMethods()) {
if ("fromValue".equals(m.getName()) && m.getParameterCount() == 1) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: to me both the method name and the PR/issue title are a bit misleading as although sounding generalized in reality there seems to be an assumption that "enum" is an enum generated by js2p.

Class<?> enumClass = Thread.currentThread().getContextClassLoader()
.loadClass(fieldType.fullName());
for (Method m : enumClass.getMethods()) {
if ("fromValue".equals(m.getName()) && m.getParameterCount() == 1) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should:

  • return type also be checked ?
  • static modifier be checked ?

{
"type" : "object",
"properties" : {
"existingEnumWithDefault" : {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it'd be possible to have a test with a "non-standard" enum value type eg. one that is backed by double or bigdecimal

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

default enum doesn't work when defining javaType

3 participants