Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimenting with #1467: Support for @JsonUnwrapped in @JsonCreators #4271

Open
wants to merge 15 commits into
base: 2.17
Choose a base branch
from

Conversation

fxshlein
Copy link
Contributor

I was more or less just messing around with an approach to solve this for now without the big refactor mentioned in the issue. Perhaps this is already fine? The test I added is green at least, but I have no idea what I might have missed. After all, if an "object with three properties" test covered a considerable amount of jackson's feature set, I wouldn't be using it... 😆

Comment on lines 86 to 88
public boolean isUnwrapped(SettableBeanProperty property) {
return this._unwrappedPropertyNames.contains(property.getName());
}
Copy link
Contributor Author

@fxshlein fxshlein Dec 20, 2023

Choose a reason for hiding this comment

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

Would this be sufficient to check for which properties are unwrapped? Not sure how different things affect the result of getName()

Copy link
Member

Choose a reason for hiding this comment

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

In theory names should be PropertyName instances (matters with XML, mostly), but internally a few things are still just Strings so I suspect this is fine.

Copy link
Member

Choose a reason for hiding this comment

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

As per my comment higher up, I am not sure this method is actually needed: at least tests included seem to pass just fine without it being called (as well as all other existing tests).

import com.fasterxml.jackson.databind.deser.SettableBeanProperty;
import com.fasterxml.jackson.databind.util.NameTransformer;
import com.fasterxml.jackson.databind.util.TokenBuffer;

import java.io.IOException;
Copy link
Member

Choose a reason for hiding this comment

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

Could you move these back to where they were? (I assume IDE just reordered them automatically).

(we finally have Coding Style Guide -> https://github.com/FasterXML/jackson/blob/master/contribution/jackson-coding-style-guide.md

that explains intended ordering)

@@ -716,6 +717,14 @@ private void _addCreatorParam(Map<String, POJOPropertyBuilder> props,
PropertyName pn = _annotationIntrospector.findNameForDeserialization(param);
boolean expl = (pn != null && !pn.isEmpty());
if (!expl) {
boolean unrwapping = _annotationIntrospector.findUnwrappingNameTransformer(param) != null;
Copy link
Member

Choose a reason for hiding this comment

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

typo in name :)

@@ -1,101 +0,0 @@
package com.fasterxml.jackson.databind.struct;
Copy link
Member

Choose a reason for hiding this comment

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

Why was this test deleted? Was it just renamed or ... ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was the test that verified that error messages were printed when using @JsonUnwrapped in a creator, which hopefully shouldn't be needed anymore :)

Copy link
Member

Choose a reason for hiding this comment

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

Ah. Yes, that makes sense. :)

/**
* Tests to verify [databind#1467].
*/
public class TestUnwrappedWithJsonCreator extends BaseMapTest
Copy link
Member

Choose a reason for hiding this comment

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

Newer naming convention leads to UnwrappedWith[Json]CreatorTest (we haven't renamed all or even most tests but trying to move in that direction).

@cowtowncoder
Copy link
Member

Whoa! Impressive work. I share your suspicion wrt test coverage (lack thereof), but it is what it is.

I'd need to read this through with more thought and focus to see if there's anything specifically that wouldn't work (or be against design). In the meantime I added some notes on code style issues (which are obviously not super important but might as well mention now).

Also, assuming we get through this to merge it, one thing needed (if not already done) is CLA:

https://github.com/FasterXML/jackson/blob/master/contributor-agreement.pdf

which we need before merging the first contribution. It's usually easiest to print, fill & sign, scan/photo, email to cla at fasterxml dot com. Only needed once, good for all future contributions.

public void testUnwrappedWithRecord() throws Exception
{
String json = "{\"unrelated\": \"unrelatedValue\", \"property1\": \"value1\", \"property2\": \"value2\"}";
RecordWithJsonUnwrapped outer = MAPPER.readValue(json, RecordWithJsonUnwrapped.class);
Copy link
Member

Choose a reason for hiding this comment

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

Works, but might make sense to first serialize from instance, then read back (round-trip). To make both directions work (I realize that this PR is just for deserialization but still)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed it to a round-trip. Should I also change the tests in UnwrappedWithCreatorTest? Since there are already similar tests for serializing regular classes.

@fxshlein
Copy link
Contributor Author

@cowtowncoder I sent the signed CLA to cla@.... 🙂

Just to verify, here it says to email the CLA to info@..., are both fine?

@@ -855,7 +855,7 @@ protected Object deserializeUsingPropertyBasedWithUnwrapped(JsonParser p, Deseri
if (buffer.readIdProperty(propName) && creatorProp == null) {
continue;
}
if (creatorProp != null) {
if (creatorProp != null && !_unwrappedPropertyHandler.isUnwrapped(creatorProp)) {
Copy link
Member

Choose a reason for hiding this comment

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

I'll have to think about this; ideally such lookup shouldn't be needed (rather, creatorProp would have knowledge of whether it's created via unwrapping).

Copy link
Member

Choose a reason for hiding this comment

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

Oddly enough, removing this call makes no difference; all tests pass the same (including Record test).

So is it actually needed? Either we should have a test that fails without it, or it should just be removed, I think.

Copy link
Contributor Author

@fxshlein fxshlein Jan 6, 2024

Choose a reason for hiding this comment

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

Yeah, it seems like this makes no sense anymore, I probably implemented it before adapting the rest of the code. The issue this would have solved is if you have a structure like this:

record Outer(@JsonUnwrapped Inner propertyName) {}

record Inner(String propertyName, String otherPropertyName) {}

There is a property called propertyName inside and outside the unwrapped structure, so when deserializing and encountering the property called "propertyName", I initially thought it would find that property. But as far as this code is concerned, the outer property is called "@JsonUnwrapped" (because this is the fake name it's given), so it should not clash either way. Because of this fake name, the _unwrappedPropertyNames set used for isUnwrapped actually only contains "@JsonUnwrapped"... 😄

Copy link
Member

Choose a reason for hiding this comment

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

Right, I thought it might just contain @JsonUnwrapped :)

protected final List<SettableBeanProperty> _properties;
protected final Set<String> _unwrappedPropertyNames;
Copy link
Member

Choose a reason for hiding this comment

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

... and if isUnwrapped() is not actually needed, can drop this Set and code that maintains it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Removed the set and the check!

public class UnwrappedWithCreatorTest extends BaseMapTest
{
static class ExplicitWithoutName {
private final String _unrelated;
Copy link
Member

Choose a reason for hiding this comment

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

@fxshlein I changed internal field names to make 100% sure test could not accidentally work due to field name matching constructor property name (since that would actually work despite final field declaration, in absence of Creator parameters).

Comment on lines +10 to +25
static class Outer {
@JsonUnwrapped(prefix = "inner-")
Inner inner;
}

static class Inner {
private final String _property;

public Inner(@JsonProperty("property") String property) {
_property = property;
}

public String getProperty() {
return _property;
}
}
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@cowtowncoder While looking at something else, I noticed that you could not do this because the NameTransformer used for unwrapping is not applied to _propertyBasedCreator, only to _beanProperties. This commit seems to fix that: d65dfac. I guess it broadly fits the scope of the PR, but I can also remove it and make a separate one. In theory its more or less unrelated.

@fxshlein
Copy link
Contributor Author

fxshlein commented Jan 7, 2024

I noticed a minor issue with the placeholder name: If you use two @JsonUnwrapped parameters in a creator without explicitly giving it a name @JsonProperty, they will be marked as duplicates, as they both get the placeholder:

Caused by: java.lang.IllegalArgumentException: Duplicate creator property "@JsonUnwrapped" (index 2 vs 3) for type ...

I added a test case here:
https://github.com/FasterXML/jackson-databind/pull/4271/files#diff-1b40e79714a359efac38026f7dfe9dbf45f4def1877d805437ba48955ae9478eR68-R96

From what I can see, there are three solutions to this:

  1. Just let it happen, maybe add a special exception message that tells users to simply distinguish the properties using @JsonProperty. On one hand this should fit right in, as the user probably has to have @JsonProperty on all parameters anyways, but on the other hand it would be kind of weird since the name is never actually used.
  2. Change the placeholder to include the index ("unnamed @JsonUnwrapped property at N"). This is the one I implemented now, which makes the test case work. This is nice since the name is more unique, and the names also double as human-readable identifiers in error messages.
  3. Do a refactor to allow unwrapped properties to bypass all the places where properties are placed in a map, using their name as a key. I started this here: fxshlein@3bd10a0, but it's already a pretty massive undertaking without even touching everything.

@cowtowncoder
Copy link
Member

To me (2), adding index for bogus name, sounds reasonable at this point. +1.

@wingsofovnia
Copy link

While looking for a stop-gap solution for this in GitHub, I found this (usage) rather straightforward solution for @JsonUnwrapped support in Kotlin data classes. Perhaps this might be helpful for you or others who seek a temp solution.

Looking forward to having this merged. Thank you for the contribution!

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.

None yet

3 participants