-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Adjust Record adapter and extend test coverage #2224
Adjust Record adapter and extend test coverage #2224
Conversation
} | ||
|
||
@Test | ||
public void testCustomAdapterForRecords() { | ||
Gson gson = new Gson(); | ||
TypeAdapter<?> recordAdapter = gson.getAdapter(unixDomainPrincipalClass); | ||
TypeAdapter<?> defaultReflectionAdapter = gson.getAdapter(UserPrincipal.class); | ||
TypeAdapter<?> defaultReflectionAdapter = gson.getAdapter(DummyClass.class); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have changed this because UserPrincipal
is an interface without any fields, so assuming that the normal reflection based adapter is used for it might be a bit brittle.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for this!
This is my first pass. I think I will need to spend more time looking at this, though.
gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java
Outdated
Show resolved
Hide resolved
gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java
Outdated
Show resolved
Hide resolved
gson/src/main/java/com/google/gson/internal/bind/ReflectiveTypeAdapterFactory.java
Outdated
Show resolved
Hide resolved
gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java
Outdated
Show resolved
Hide resolved
gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java
Outdated
Show resolved
Hide resolved
gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java
Show resolved
Hide resolved
gson/src/main/java/com/google/gson/internal/reflect/ReflectionHelper.java
Outdated
Show resolved
Hide resolved
accessor = ReflectionHelper.getAccessor(raw, field); | ||
if (isRecord) { | ||
// If there is a static field on a record, there will not be an accessor. Instead we will use the default | ||
// field logic for dealing with statics. For deserialization the field is ignored. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to clarify: by default, static fields in records will be neither serialized nor deserialized, right? Only if that weird excludeFieldsWithModifiers
method is called? Because I definitely don't think static fields should be serialized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that is correct, see default excluded modifiers here:
private int modifiers = Modifier.TRANSIENT | Modifier.STATIC; |
I have mainly added this to handle it in a reasonable way instead of failing with some obscure exception.
For static fields maybe there are legit (opt-in) use cases, e.g. to automatically encode a version number:
record Response(
...
) {
public static final int VERSION = 1;
}
Though I am not sure if someone is actually using this, and it is certainly not obvious (and probably not even officially supported) to use excludeFieldsWithModifiers
for this.
If you want I can adjust it to also always exclude static fields on serialization, and we can then wait for someone to request this feature.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, I think by default static fields should not be serialized. I suppose they should be if someone calls excludeFieldsWithModifiers(0)
, though even then I don't really understand why people would want that. I'm not really convinced by the VERSION
use case. We would serialize that field, and then what would we do on deserialization? Do we check that the version matches? Do we try to overwrite the VERSION
field? If we don't, isn't that inconsistent? I think if you really want a version then it should either be an instance field or you should use a custom TypeAdapter
.
(I looked at the history, and apparently excludeFieldsWithModifiers
was already present in the first version on GitHub, dated 2008. So who knows why it was added. For what it's worth, Google's code has only a handful of calls to this method, and exactly one that is including STATIC
, perhaps accidentally.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not really convinced by the
VERSION
use case. We would serialize that field, and then what would we do on deserialization?
It would probably be most useful for services which only serialize data, e.g. a REST API which generates the response. At least that is the most reasonable situation I can come up with at the moment.
It appears the excludeFieldsWithModifiers
trick is known, see this StackOverflow answer.
public void testStaticFieldSerialization() { | ||
Gson gson = new GsonBuilder() | ||
// Include static fields | ||
.excludeFieldsWithModifiers(0) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you also test that the field is not serialized without this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It looks like there was already a test for this: com.google.gson.functional.UncategorizedTest.testStaticFieldsAreNotSerialized()
And additionally multiple tests implicitly relied on static
fields being ignored by default. However, I have now extended these new test methods to also test the default Gson behavior.
Also, I have noticed that Field.set
per documentation does not support static final
fields. So to avoid Gson claiming "Unexpected IllegalAccessException occurred...", I have added custom handling for that case to provide a better exception message.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(It was unclear to me whether there is in fact test coverage for static fields not being serialized by default.)
Previously it would report "Unexpected IllegalAccessException occurred..." due to the uncaught IllegalAccessException.
Such an exception is not 'unexpected' (which was claimed by the previous exception handling) because user code could throw it.
d72a9d1
to
e28ad83
Compare
Sorry for these additional commits. I did not consider these cases initially and only noticed now that they are not covered by any tests. I hope the additional commits I pushed are fine. |
/** Tests behavior when the canonical constructor throws an exception */ | ||
@Test | ||
public void testThrowingConstructor() { | ||
record LocalRecord(String s) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It appears the Eclipse compiler produces malformed byte code for this record class (have not investigated that further yet). Just as forewarning in case you are experiencing a java.lang.VerifyError
when running this test in Eclipse.
Edit: Have created eclipse-jdt/eclipse.jdt.core#487 for that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks good to me. Let me know if you think it is ready to be merged.
It should be ready to merge. In case you plan to create a release soon (per Semantic Versioning that would be 2.10.0 I assume), it would be good if you could take a look at #2212. If those changes are too experimental / hacky in your opinion, then at least during the release process the |
Changes:
null
for primitive component, see Small adjustments to the new record code. #2219 (comment)static
fields when deserializing recordsWould not work without special casing and I am not sure if serializing or deserializing
static
fields was ever officially documented as supported (just happens to work by usingGsonBuilder.excludeFieldsWithModifiers
). For serialization there might be legit use cases (e.g. to encode a version number), but deserialization it rather questionable.@SerializedName
placed only on accessor methodImplemented this because we have the accessor method available for this check anyway, and because for records the accessor method is actually called on serialization so users might expect
@SerializedName
to work on them.ReflectionHelper.makeAccessible
to create description of object only when exception is thrown instead of eagerlyReflectiveTypeAdapterFactoryTest
andReflectionHelperTest
withJava17
This makes sure that we notice when the checked JDK class
UnixDomainPrincipal
is missing for some reason even though the test runs on Java >= 17. Otherwise these tests might just be silently skipped without us noticing.Arguably these tests could also be rewritten to use custom record classes now that the tests are only compiled on Java 17 or newer, but on the other hand it might also be valuable that they test against a record class which is not part of Gson's tests.