You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Thank you for raising this. Indeed, things seem to have changed in Java 15. The fact that records are immutable is actually incompatible with the way Easy Random works which is creating an a new ("empty") instance of the target type and then populating its fields afterwards. With records, it should be the other way around: generate random values for record components (ie fields) then create an instance with those values. So I think this feature should not be implemented in Easy Random itself (given the impact this has on the current code/approach), but using a custom ObjectFactory for records. Here is a quick example:
packageorg.jeasy.random;
importjava.lang.reflect.Constructor;
importjava.lang.reflect.RecordComponent;
importorg.jeasy.random.api.RandomizerContext;
publicclassRecordFactoryextendsObjenesisObjectFactory {
privateEasyRandomeasyRandom;
@Overridepublic <T> TcreateInstance(Class<T> type, RandomizerContextcontext) {
if (easyRandom == null) {
easyRandom = newEasyRandom(context.getParameters());
}
if (type.isRecord()) {
returncreateRandomRecord(type);
} else {
returnsuper.createInstance(type, context);
}
}
private <T> TcreateRandomRecord(Class<T> recordType) {
// generate random values for record componentsRecordComponent[] recordComponents = recordType.getRecordComponents();
Object[] randomValues = newObject[recordComponents.length];
for (inti = 0; i < recordComponents.length; i++) {
randomValues[i] = easyRandom.nextObject(recordComponents[i].getType());
}
// create a random instance with random valuestry {
returngetCanonicalConstructor(recordType).newInstance(randomValues);
} catch (Exceptione) {
thrownewObjectCreationException("Unable to create a random instance of recordType " + recordType, e);
}
}
private <T> Constructor<T> getCanonicalConstructor(Class<T> recordType) {
RecordComponent[] recordComponents = recordType.getRecordComponents();
Class<?>[] componentTypes = newClass<?>[recordComponents.length];
for (inti = 0; i < recordComponents.length; i++) {
// recordComponents are ordered, see javadoc:// "The components are returned in the same order that they are declared in the record header"componentTypes[i] = recordComponents[i].getType();
}
try {
returnrecordType.getDeclaredConstructor(componentTypes);
} catch (NoSuchMethodExceptione) {
// should not happen, from Record javadoc:// "A record class has the following mandated members: a public canonical constructor ,// whose descriptor is the same as the record descriptor;"thrownewRuntimeException("Invalid record definition", e);
}
}
}
With this custom factory, the previous example works with Java 15 (assuming that the custom factory is passed to ER through parameters):
Thank you @benas. Are you sure that this should not be enabled by default for records (seeing as they will become a standard feature should easy-random not support them out of the box)? What would be the negatives of doing so?
@vab2048 It should, this is what I had in mind when I first created this issue. The idea was that easy random should be able to handle both regular classes and records in a transparent way. This was true with Java 14 (as shown in #397 (comment)), but unfortunately not anymore with Java 15 as reported by @mmaggioni-wise (it's ok that things break since java records are still in preview).
Records will be out of preview in Java 16. This means adding record support in ER in a transparent way (ie without a custom object factory) requires a major release that is based on Java 16 (there are techniques to do conditional compilation &co but I'm not ready to deal with that here). Even though that's fine in my opinion, it is a quite aggressive requirement, I remember some users were reluctant to making ER v5 based on Java 11 already, see dfbab94#r43985189).
But the real issue is not about the minimum required Java version. The issue is that since java records are immutable, they should be randomized in the opposite way of randomizing java beans (ie first generate random values then create a record instance with them vs create an empty bean instance then set random values on it). This requires a substantial amount of work to implement and test. I think it's unfortunate that ER does not support records OOTB after being declared in maintenance mode (this decision was made based on the assumption that records are already supported, which, I must admit, is a mistake since I should have expected things to break before this feature goes out of preview..). So I think it's wise to re-consider records support in ER v6 which would be based on Java 16 (in which records will be stable and we can expect no breaking changes as the ones introduced between 14 and 15). wdyt?
@benas your idea of waiting until records become a standard feature makes perfect sense. With Java 16 due to be released in March 2021 and records becoming a standard feature (https://openjdk.java.net/projects/jdk/16/) perhaps this issue should remain open as a reminder for the future once it is released?
Until then the code example you provided works wonderfully with Java 15 and I suspect would also work with Java 16 - so we have an easy workaround.
Initial support for records has been added and will be released in v6. As mentioned in my previous comment, the idea is to make ER handle both beans and records in a transparent way. This is now possible, and there is no need to register a custom object factory for records.
v6.0.0 snapshots are available now, and I would be grateful to anyone interested in giving this a try and reporting any problem in a separate issue. There might be a few edge cases that should be tested in depth (recursion, composition, etc), but the goal here is iterate on the feature. @vab2048 thank you for reporting this issue, I created #496 for further investigation.
For those wondering why this was not addressed when Java 16 was out, here is the reason: while I could have released ER6 right after Java 16 was released (in which records were finalized), I thought basing ER 6 on an LTS Java release made more sense. Hence, ER 6 will be based on Java 17 (#495).
As a side note, it is great to see other people working on different/similar solutions in the same space! However, this is not the place to do issue management on forks or to promote other projects. I have hidden all comments that are off-topic. If anyone is interested in promoting a fork or a similar project, please reach out to me and I would be more than happy to mention it in the README. Thank you all!
Hello ! I haven't tested the v6.0.0 snapshot but I am using your workaround posted above. I found an issue with that workaround. I have a record class with contains a list of another record object (besides other fields). When I tried to generate a random object, that list is null. Not sure if this is already addressed in the v6.0.0. I tried to find another workaround for this, and here is my solution (if you will add it in v6.0.0 the code will be much simpler).
@SneakyThrows
@SuppressWarnings({"unchecked", "rawtypes"})
private <T> T createRandomRecord(Class<T> recordType, RandomizerContext context) {
// generate random values for record components
var recordComponents = recordType.getRecordComponents();
var randomValues = new Object[recordComponents.length];
for (int i = 0; i < recordComponents.length; i++) {
if (isCollectionType(recordComponents[i].getType())) {
var randomSize = new IntegerRangeRandomizer(context.getParameters().getCollectionSizeRange().getMin(), context.getParameters().getCollectionSizeRange().getMax(), easyRandom.nextLong()).getRandomValue();
Collection collection;
if (isInterface(recordComponents[i].getType())) {
collection = getEmptyImplementationForCollectionInterface(recordComponents[i].getType());
} else {
collection = createEmptyCollectionForType(recordComponents[i].getType(), randomSize);
}
for (int j = 0; j < randomSize; j++) {
collection.add(createRandomRecord((Class<?>) (((ParameterizedType) (recordType.getDeclaredFields()[0].getGenericType())).getActualTypeArguments()[0]), context));
}
randomValues[i] = collection;
} else {
randomValues[i] = easyRandom.nextObject(recordComponents[i].getType());
}
}
// create a random instance with random values
try {
return getCanonicalConstructor(recordType).newInstance(randomValues);
} catch (Exception e) {
throw new ObjectCreationException("Unable to create a random instance of recordType " + recordType, e);
}
}
I seem to have run into an issue generating random record objects. It seems that generation fails when the record object has a field of type Optional. The error occurs in the v6 snapshot and the factory mentioned in the comment. Using the factory and v5 version of the library the error is:
The error does not occur when trying to create a random instance of a class with a field of type Optional. I think the problem comes from using EasyRandom.nextObject to populate record fields instead of the FieldPopulator.populateField. FieldPopulator.generateRandomValue has different checks for field types which EasyRandom.nextObject lacks.
Activity
[-]Add support to generate random Records added in Java 14[/-][+]Add support to generate random Java 14 Records[/+]fmbenhassine commentedon Nov 8, 2020
Since records are classes, EasyRandom is actually able to randomize them. Here is an example that works as expected with ER 4.3 and Java 14:
Minimal complete example: test-easy-random-records.zip
mmaggioni-wise commentedon Nov 9, 2020
Hi @benas ,
it doesn't work with java 15.
it seems it's not possibile anymore to change record fields through reflection.
java.lang.IllegalAccessException: Can not set final fieldhttps://mail.openjdk.java.net/pipermail/amber-dev/2020-June/006241.html
https://medium.com/@atdsqdjyfkyziqkezu/java-15-breaks-deserialization-of-records-902fcc81253d
fmbenhassine commentedon Nov 14, 2020
Hi @mmaggioni-wise ,
Thank you for raising this. Indeed, things seem to have changed in Java 15. The fact that records are immutable is actually incompatible with the way Easy Random works which is creating an a new ("empty") instance of the target type and then populating its fields afterwards. With records, it should be the other way around: generate random values for record components (ie fields) then create an instance with those values. So I think this feature should not be implemented in Easy Random itself (given the impact this has on the current code/approach), but using a custom
ObjectFactoryfor records. Here is a quick example:With this custom factory, the previous example works with Java 15 (assuming that the custom factory is passed to ER through parameters):
Minimal complete example for reference: test-easy-random-records-custom-factory.zip
mmaggioni-wise commentedon Nov 16, 2020
thank you @benas for your answer and your example!
vab2048 commentedon Dec 5, 2020
Thank you @benas. Are you sure that this should not be enabled by default for records (seeing as they will become a standard feature should easy-random not support them out of the box)? What would be the negatives of doing so?
[-]Add support to generate random Java 14 Records[/-][+]Add support to generate random Java Records[/+]fmbenhassine commentedon Dec 13, 2020
@vab2048 It should, this is what I had in mind when I first created this issue. The idea was that easy random should be able to handle both regular classes and records in a transparent way. This was true with Java 14 (as shown in #397 (comment)), but unfortunately not anymore with Java 15 as reported by @mmaggioni-wise (it's ok that things break since java records are still in preview).
Records will be out of preview in Java 16. This means adding record support in ER in a transparent way (ie without a custom object factory) requires a major release that is based on Java 16 (there are techniques to do conditional compilation &co but I'm not ready to deal with that here). Even though that's fine in my opinion, it is a quite aggressive requirement, I remember some users were reluctant to making ER v5 based on Java 11 already, see dfbab94#r43985189).
But the real issue is not about the minimum required Java version. The issue is that since java records are immutable, they should be randomized in the opposite way of randomizing java beans (ie first generate random values then create a record instance with them vs create an empty bean instance then set random values on it). This requires a substantial amount of work to implement and test. I think it's unfortunate that ER does not support records OOTB after being declared in maintenance mode (this decision was made based on the assumption that records are already supported, which, I must admit, is a mistake since I should have expected things to break before this feature goes out of preview..). So I think it's wise to re-consider records support in ER v6 which would be based on Java 16 (in which records will be stable and we can expect no breaking changes as the ones introduced between 14 and 15). wdyt?
vab2048 commentedon Dec 23, 2020
@benas your idea of waiting until records become a standard feature makes perfect sense. With Java 16 due to be released in March 2021 and records becoming a standard feature (https://openjdk.java.net/projects/jdk/16/) perhaps this issue should remain open as a reminder for the future once it is released?
Until then the code example you provided works wonderfully with Java 15 and I suspect would also work with Java 16 - so we have an easy workaround.
17 remaining items
mjureczko commentedon Mar 10, 2023
dvgaba commentedon Mar 10, 2023
GabrielBB commentedon Mar 11, 2023
dvgaba commentedon Mar 12, 2023
carborgar commentedon Mar 15, 2023
dvgaba commentedon Mar 19, 2023
carborgar commentedon Mar 19, 2023
dvgaba commentedon Mar 19, 2023
GoodforGod commentedon Apr 19, 2023
fmbenhassine commentedon Jun 30, 2023
Initial support for records has been added and will be released in v6. As mentioned in my previous comment, the idea is to make ER handle both beans and records in a transparent way. This is now possible, and there is no need to register a custom object factory for records.
v6.0.0 snapshots are available now, and I would be grateful to anyone interested in giving this a try and reporting any problem in a separate issue. There might be a few edge cases that should be tested in depth (recursion, composition, etc), but the goal here is iterate on the feature. @vab2048 thank you for reporting this issue, I created #496 for further investigation.
For those wondering why this was not addressed when Java 16 was out, here is the reason: while I could have released ER6 right after Java 16 was released (in which records were finalized), I thought basing ER 6 on an LTS Java release made more sense. Hence, ER 6 will be based on Java 17 (#495).
As a side note, it is great to see other people working on different/similar solutions in the same space! However, this is not the place to do issue management on forks or to promote other projects. I have hidden all comments that are off-topic. If anyone is interested in promoting a fork or a similar project, please reach out to me and I would be more than happy to mention it in the README. Thank you all!
cosmintudora commentedon Aug 3, 2023
Hello ! I haven't tested the v6.0.0 snapshot but I am using your workaround posted above. I found an issue with that workaround. I have a record class with contains a list of another record object (besides other fields). When I tried to generate a random object, that list is null. Not sure if this is already addressed in the v6.0.0. I tried to find another workaround for this, and here is my solution (if you will add it in v6.0.0 the code will be much simpler).
johan-miller commentedon Oct 5, 2023
Hi,
I seem to have run into an issue generating random record objects. It seems that generation fails when the record object has a field of type Optional. The error occurs in the v6 snapshot and the factory mentioned in the comment. Using the factory and v5 version of the library the error is:
The error does not occur when trying to create a random instance of a class with a field of type Optional. I think the problem comes from using
EasyRandom.nextObjectto populate record fields instead of theFieldPopulator.populateField.FieldPopulator.generateRandomValuehas different checks for field types whichEasyRandom.nextObjectlacks.carborgar commentedon Oct 5, 2023
@johan-miller you should not use Optional as a class field type. Instead, use a getter for that nullable value. Something like that