Skip to content

Conversation

@mpritham
Copy link
Contributor

@mpritham mpritham commented Dec 15, 2025

Before this PR

The union objects generated when the sealedUnions flag is set create toString methods that are not backward-compatible with objects generated without the sealedUnions flag. While in general toString method implementation is not part of the Conjure API, it's current use in the serialization of error parameters makes rolling out changes to toString a wire break.

After this PR

==COMMIT_MSG==
[Sealed unions] Make toString backward compatible
==COMMIT_MSG==

Possible downsides?

Clunky and inaccurate. The toString representation of union objects no longer accurately represents the generated object: there will be <Variant>Wrapperstrings in thetoString` representation which do not correspond to any class in the sealed class implementation.

.addMethod(MethodSpecs.createToString(
unionClass.simpleName(),
fields.stream()
valueFieldList.stream()
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unneeded traversal.

Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure I understand this comment, nor why you made the above change, as it doesn't seem like you're using valueField anywhere?

.addMethod(MethodSpecs.createToString(
unionClass.simpleName(),
fields.stream()
valueFieldList.stream()
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure I understand this comment, nor why you made the above change, as it doesn't seem like you're using valueField anywhere?

Comment on lines 225 to 253
if (sealedUnions) {
return MethodSpec.methodBuilder("toString")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(ClassName.get(String.class))
.addCode(CodeBlock.builder()
.add(
"return $S\n",
baseClassName.simpleName() + '{' + VALUE_FIELD_NAME + ": "
+ wrapperClassName.simpleName() + '{' + VALUE_FIELD_NAME + ": ")
.add(" + $N", VALUE_FIELD_NAME)
.add(" + \"}}\";")
.build())
.addJavadoc(
"This method is not part of Conjure API. Users should not rely on consistent generation of"
+ " the toString method between versions of Conjure.")
.build();
} else {
return MethodSpec.methodBuilder("toString")
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.returns(ClassName.get(String.class))
.addCode(CodeBlock.builder()
.add("return $S\n", wrapperClassName.simpleName() + '{' + VALUE_FIELD_NAME + ": ")
.add(" + $N", VALUE_FIELD_NAME)
.add(" + '}';")
.build())
.build();
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Something slightly bugs me in that we are hardcoding the expectation that there is only a single field. I'm not seeing this change in the near-term, but I'm wondering whether we could somehow rely on MethodSpecs.createToString or at least have a similar logic?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

A few ideas:

  • Have some nested structure passed to MethodSpecs.createToString that can represent arbitrarily nested lists
sealed interface ToStringField permits SimpleField, NestedField {}
record SimpleField(FieldName name) implements ToStringField {}
record NestedField(FieldName name, List<ToStringField> children) implements ToStringField {}

I like the design of this, but I didn't follow through with the idea because it made things more complicated for just this one use-case. I think we should explore this if we need to make other nested toString methods.

  • Pass in a map that provides a CodeBlock toMethodSpecs.createToString containing the representation of the field.
private static CodeBlock toStringConcatenation(
          String thisClassName,
          List<FieldName> fieldNames,
          Map<FieldName, CodeBlock> customFieldBlocks) {...}

This feels like a worse version of the idea above: the caller has to copy the logic in MethodSpecs.toStringConcatenation.

Maybe there's another thing you had in mind?

Copy link
Contributor

Choose a reason for hiding this comment

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

Having special handling for the legacy case seems fine actually. I can't seem to figure out a good way that isn't over-engineered. Relying on the existing method for the normal case seems better though (which I see you've done).

One thing we could add would be a MethodSpecs.createToStringBuilder(CodeBlock) -> MethodSpec.Builder to encode the

MethodSpec.methodBuilder("toString")
                .addAnnotation(Override.class)
                .addModifiers(Modifier.PUBLIC)
                .returns(ClassName.get(String.class))

part of the logic, but I don't feel strongly about it and it does clutter the code a bit

Comment on lines 238 to 240
.addJavadoc(
"This method is not part of Conjure API. Users should not rely on consistent generation of"
+ " the toString method between versions of Conjure.")
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we just always have this javadoc?

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure we should include this javadoc in the generation, feels a bit out of place just being on sealed union types. As long as we have a plan to migrate away from the old toString, we should just revert this later once safe.

Copy link
Contributor

Choose a reason for hiding this comment

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

This makes it somewhat explicit that people shouldn't be relying on toString's format in general, rather than just because of this specific case

Copy link
Contributor Author

@mpritham mpritham Dec 18, 2025

Choose a reason for hiding this comment

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

Adding this javadoc to all object toString methods is quite a large diff and somewhat unrelated to this PR. I can open a separate PR adding the javadoc to the generated code in other object types.

Copy link
Contributor

Choose a reason for hiding this comment

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

As a counter point, there's a lot of things explicitly encoded in our conjure spec that aren't directly included in code/javadocs. Given this is true for all toStrings, it feels better to document this once as opposed to on every conjure object's toString method. I think generally generated code should be just pure output based on the input conjure.

Copy link
Contributor

Choose a reason for hiding this comment

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

That makes sense. And it's general bad practice to rely on a specific toString format too, so maybe let's not bother

Comment on lines 216 to 224
/** Creates a backward compatible toString method. While in general, Conjure-Java does not provide any guarantees on
* the toString representation of generated objects, the toString method is currently used when serializing error
* parameters when JSON is not explicitly used. This is clunky because the sealedUnions implementation does not
* generate any classes suffixed with `Wrapper`, yet such classes show up in the `toString` implementation. In a
* future Conjure-Java change, we can update the toString method here to better represent the variant in the
* sealedUnions implementation.
*/
private static MethodSpec createToString(
boolean sealedUnions, ClassName baseClassName, ClassName wrapperClassName) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we add some tests to guarantee that this stays true?

"This method is not part of Conjure API. Users should not rely on consistent generation of"
+ " the toString method between versions of Conjure.")
.build();
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

why would we not continue to use MethodSpecs#createToString for at least the old union types?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated. I was trying to create a method that handles only the case where we have value as a field, but it ended up being not as clean as just using the old logic in MethodSpecs

Comment on lines 238 to 240
.addJavadoc(
"This method is not part of Conjure API. Users should not rely on consistent generation of"
+ " the toString method between versions of Conjure.")
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure we should include this javadoc in the generation, feels a bit out of place just being on sealed union types. As long as we have a plan to migrate away from the old toString, we should just revert this later once safe.

@mpritham mpritham force-pushed the mpritham/sealed-unions-toString-p2 branch 2 times, most recently from dc99860 to 15dc067 Compare December 18, 2025 04:08
Copy link
Contributor

@aldexis aldexis left a comment

Choose a reason for hiding this comment

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

The current approach seems reasonable to me, provided we do want to do this. If/when we confirm and you remove the do-not-merge, I'm happy to approve

@mpritham mpritham force-pushed the mpritham/sealed-unions-toString-p2 branch from 15dc067 to acb0f8d Compare December 18, 2025 16:16
@mpritham mpritham changed the base branch from mpritham/sealed-unions-toString to develop December 18, 2025 16:16
@changelog-app
Copy link

changelog-app bot commented Dec 18, 2025

Generate changelog in changelog/@unreleased

Type (Select exactly one)

  • Feature (Adding new functionality)
  • Improvement (Improving existing functionality)
  • Fix (Fixing an issue with existing functionality)
  • Break (Creating a new major version by breaking public APIs)
  • Deprecation (Removing functionality in a non-breaking way)
  • Migration (Automatically moving data/functionality to a new system)

Description

[Sealed unions] Make toString backward compatible

Check the box to generate changelog(s)

  • Generate changelog entry

@mpritham mpritham changed the title [DNM] [Sealed unions] Make toString backward compatible [Sealed unions] Make toString backward compatible Dec 18, 2025
@changelog-app
Copy link

changelog-app bot commented Dec 18, 2025

Successfully generated changelog entry!

Need to regenerate?

Simply interact with the changelog bot comment again to regenerate these entries.


📋Changelog Preview

💡 Improvements

  • [Sealed unions] Make toString backward compatible (#2753)

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.

4 participants