-
-
Notifications
You must be signed in to change notification settings - Fork 9.4k
Support Record types in XStream2 #26354
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
base: master
Are you sure you want to change the base?
Changes from 6 commits
f03431e
5325fdc
4f41053
963ea33
7e4426e
00ec81d
1e1ecf2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -285,11 +285,101 @@ | |||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| @Override | ||||||||||||||||||||||||||||||||||||||
| public Object unmarshal(final HierarchicalStreamReader reader, final UnmarshallingContext context) { | ||||||||||||||||||||||||||||||||||||||
| String readResolveValue = reader.getAttribute(mapper.aliasForAttribute("resolves-to")); | ||||||||||||||||||||||||||||||||||||||
| Class type = readResolveValue != null ? mapper.realClass(readResolveValue) : context.getRequiredType(); | ||||||||||||||||||||||||||||||||||||||
| if (type != null && type.isRecord()) { | ||||||||||||||||||||||||||||||||||||||
| return unmarshalRecord(reader, context, type); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Object result = instantiateNewInstance(reader, context); | ||||||||||||||||||||||||||||||||||||||
| result = doUnmarshal(result, reader, context); | ||||||||||||||||||||||||||||||||||||||
| return serializationMethodInvoker.callReadResolve(result); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| private Object unmarshalRecord(final HierarchicalStreamReader reader, final UnmarshallingContext context, Class type) { | ||||||||||||||||||||||||||||||||||||||
| Map<String, Object> values = new HashMap<>(); | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| Iterator it = reader.getAttributeNames(); | ||||||||||||||||||||||||||||||||||||||
| while (it.hasNext()) { | ||||||||||||||||||||||||||||||||||||||
| String attrAlias = (String) it.next(); | ||||||||||||||||||||||||||||||||||||||
| String attrName = mapper.attributeForAlias(attrAlias); | ||||||||||||||||||||||||||||||||||||||
| Field field = reflectionProvider.getFieldOrNull(type, attrName); | ||||||||||||||||||||||||||||||||||||||
| if (field != null) { | ||||||||||||||||||||||||||||||||||||||
| SingleValueConverter converter = mapper.getConverterFromAttribute(field.getDeclaringClass(), attrName, field.getType()); | ||||||||||||||||||||||||||||||||||||||
| Class fieldType = field.getType(); | ||||||||||||||||||||||||||||||||||||||
| if (converter == null) { | ||||||||||||||||||||||||||||||||||||||
| converter = mapper.getConverterFromItemType(fieldType); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| if (converter != null) { | ||||||||||||||||||||||||||||||||||||||
| Object value = converter.fromString(reader.getAttribute(attrAlias)); | ||||||||||||||||||||||||||||||||||||||
| if (fieldType.isPrimitive()) { | ||||||||||||||||||||||||||||||||||||||
| fieldType = Primitives.box(fieldType); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| if (value != null && !fieldType.isAssignableFrom(value.getClass())) { | ||||||||||||||||||||||||||||||||||||||
| throw new ConversionException("Cannot convert type " + value.getClass().getName() + " to type " + fieldType.getName()); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| values.put(attrName, value); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| while (reader.hasMoreChildren()) { | ||||||||||||||||||||||||||||||||||||||
| reader.moveDown(); | ||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||
| String fieldName = mapper.realMember(type, reader.getNodeName()); | ||||||||||||||||||||||||||||||||||||||
| Field field = reflectionProvider.getFieldOrNull(type, fieldName); | ||||||||||||||||||||||||||||||||||||||
| if (field != null) { | ||||||||||||||||||||||||||||||||||||||
| Class fieldType = field.getType(); | ||||||||||||||||||||||||||||||||||||||
| Class xmlType = mapper.defaultImplementationOf(fieldType); | ||||||||||||||||||||||||||||||||||||||
| String classAttribute = reader.getAttribute(mapper.aliasForAttribute("class")); | ||||||||||||||||||||||||||||||||||||||
| if (classAttribute != null) { | ||||||||||||||||||||||||||||||||||||||
| Class specifiedType = mapper.realClass(classAttribute); | ||||||||||||||||||||||||||||||||||||||
| if (fieldType.isAssignableFrom(specifiedType)) { | ||||||||||||||||||||||||||||||||||||||
| xmlType = specifiedType; | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| Object value = unmarshalField(context, null, xmlType, field); | ||||||||||||||||||||||||||||||||||||||
| if (value != null && !xmlType.isAssignableFrom(value.getClass())) { | ||||||||||||||||||||||||||||||||||||||
| LOGGER.warning("Cannot convert type " + value.getClass().getName() + " to type " + xmlType.getName()); | ||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||
| values.put(fieldName, value); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+347
to
+377
|
||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||
| Class itemType = mapper.getItemTypeForItemFieldName(type, fieldName); | ||||||||||||||||||||||||||||||||||||||
| Class xmlType = itemType != null ? itemType : mapper.realClass(reader.getNodeName()); | ||||||||||||||||||||||||||||||||||||||
| context.convertAnother(null, xmlType); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| } catch (CriticalXStreamException | InputManipulationException e) { | ||||||||||||||||||||||||||||||||||||||
| throw e; | ||||||||||||||||||||||||||||||||||||||
| } catch (XStreamException | LinkageError e) { | ||||||||||||||||||||||||||||||||||||||
| addErrorInContext(context, e); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
| reader.moveUp(); | ||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+356
to
+389
|
||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||
| java.lang.reflect.RecordComponent[] components = type.getRecordComponents(); | ||||||||||||||||||||||||||||||||||||||
| Class[] parameterTypes = new Class[components.length]; | ||||||||||||||||||||||||||||||||||||||
| Object[] args = new Object[components.length]; | ||||||||||||||||||||||||||||||||||||||
| for (int i = 0; i < components.length; i++) { | ||||||||||||||||||||||||||||||||||||||
| java.lang.reflect.RecordComponent component = components[i]; | ||||||||||||||||||||||||||||||||||||||
| Class pType = component.getType(); | ||||||||||||||||||||||||||||||||||||||
| String name = component.getName(); | ||||||||||||||||||||||||||||||||||||||
| parameterTypes[i] = pType; | ||||||||||||||||||||||||||||||||||||||
| Object val = values.get(name); | ||||||||||||||||||||||||||||||||||||||
| if (val == null && pType.isPrimitive()) { | ||||||||||||||||||||||||||||||||||||||
| val = java.lang.reflect.Array.get(java.lang.reflect.Array.newInstance(pType, 1), 0); | ||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||
| val = java.lang.reflect.Array.get(java.lang.reflect.Array.newInstance(pType, 1), 0); | |
| if (pType == boolean.class) { | |
| val = false; | |
| } else if (pType == char.class) { | |
| val = '\0'; | |
| } else if (pType == byte.class) { | |
| val = (byte) 0; | |
| } else if (pType == short.class) { | |
| val = (short) 0; | |
| } else if (pType == int.class) { | |
| val = 0; | |
| } else if (pType == long.class) { | |
| val = 0L; | |
| } else if (pType == float.class) { | |
| val = 0.0f; | |
| } else if (pType == double.class) { | |
| val = 0.0d; | |
| } |
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's a one-liner that handles all 8 primitive types without a switch/map. I've seen this pattern used in a few places in JDK internals. A map would be more readable but also more code. Can change it if you prefer but it's not really hurting anything.
Outdated
Copilot
AI
Feb 23, 2026
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.
The unmarshalRecord method does not handle Saveable error reporting. Unlike doUnmarshal (lines 486-505), this method doesn't check if the record implements Saveable and report any ReadErrors via OldDataMonitor. If a record type implements Saveable and has errors during deserialization, those errors will not be tracked or reported, leading to inconsistent error handling compared to regular classes.
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.
Records are value types , they get embedded inside Saveable objects, they don't implement Saveable themselves. The outer object's doUnmarshal still handles OldDataMonitor reporting, so errors still get tracked.
Outdated
Copilot
AI
Feb 23, 2026
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.
This PR adds significant new functionality for record support but does not include any tests in RobustReflectionConverterTest.java. Based on the existing test coverage patterns in the codebase, comprehensive tests should be added to verify: 1) basic record unmarshaling with various field types, 2) records with primitive fields (to test the default value logic at lines 370-372), 3) records with null fields, 4) error handling for malformed XML, 5) attribute-based fields in records, 6) child element fields in records, and 7) any readResolve behavior if applicable.
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.
Already added in a follow-up commit , four tests covering basic unmarshal, missing field defaults, extra XML elements, and full round-trip through XStream2.
Outdated
Copilot
AI
Feb 23, 2026
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.
The unmarshalRecord method does not call serializationMethodInvoker.callReadResolve on the returned record instance. Records can have readResolve methods, and the standard unmarshal path calls this method at line 296. Without this call, record deserialization will not properly invoke any readResolve methods defined on record types, which could break existing functionality if records with readResolve are used.
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.
Good catch, missed this one. I'll add it before the return. Pushing a fix.
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.
The unmarshalRecord method lacks JavaDoc documentation. Given that this is a significant new private method handling a distinct code path for records, it should include documentation explaining its purpose, parameters, return value, and any exceptions it may throw. This would be particularly helpful for maintainability, as the method implements complex deserialization logic that differs from the standard doUnmarshal path.
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.
Yeah makes sense, I'll add a doc comment for it since the deserialization path for records is different enough from doUnmarshal. Will push.