@@ -121,6 +121,37 @@ public class FromXmlParser
121121 */
122122 protected boolean _nextIsNullXsiNil ;
123123
124+ /*
125+ /**********************************************************************
126+ /* Parsing state, optional root element wrapping (3.2)
127+ /**********************************************************************
128+ */
129+
130+ // [dataformat-xml#484] Root-element wrap state machine. When
131+ // {@link XmlReadFeature#WRAP_ROOT_ELEMENT_NAME} is enabled, the parser
132+ // synthesizes an extra outer Object whose single property is the root
133+ // element local name. {@code _rootWrapStage} drives the synthetic tokens.
134+
135+ /** Wrap inactive: feature off, or wrapper already fully delivered. */
136+ private static final int WRAP_INACTIVE = 0 ;
137+ /** Next call emits the synthetic outer START_OBJECT. */
138+ private static final int WRAP_PENDING_OUTER_START = 1 ;
139+ /** Next call emits the synthetic PROPERTY_NAME (root local name). */
140+ private static final int WRAP_PENDING_NAME = 2 ;
141+ /** Wrapper open over an Object body: emit synthetic END_OBJECT when stream reaches XML_END. */
142+ private static final int WRAP_OPEN_OBJECT = 3 ;
143+ /** Wrapper open over a scalar/null body: queued in {@code _nextToken}; transition to PENDING_OUTER_END after delivery. */
144+ private static final int WRAP_OPEN_SCALAR = 4 ;
145+ /** Next call emits the synthetic outer END_OBJECT (scalar/null body case). */
146+ private static final int WRAP_PENDING_OUTER_END = 5 ;
147+
148+ /**
149+ * Stage of the root-element wrap state machine; see WRAP_* constants above.
150+ *
151+ * @since 3.2
152+ */
153+ protected int _rootWrapStage = WRAP_INACTIVE ;
154+
124155 /*
125156 /**********************************************************************
126157 /* Parsing state, parsed values
@@ -207,6 +238,7 @@ public FromXmlParser(ObjectReadContext readCtxt, IOContext ioCtxt,
207238
208239 // 04-Jan-2019, tatu: Root-level nulls need slightly specific handling;
209240 // changed in 2.10.2
241+ final boolean wrapRoot = isEnabled (XmlReadFeature .WRAP_ROOT_ELEMENT_NAME );
210242 if (_xmlTokens .hasXsiNil ()) {
211243 _nextToken = JsonToken .VALUE_NULL ;
212244 // 21-Apr-2025, tatu: [dataformat-xml#714] Must "flush" the stream
@@ -216,7 +248,12 @@ public FromXmlParser(ObjectReadContext readCtxt, IOContext ioCtxt,
216248 case XmlTokenStream .XML_START_ELEMENT :
217249 // Removed from 2.14:
218250 // case XmlTokenStream.XML_DELAYED_START_ELEMENT:
219- _nextToken = JsonToken .START_OBJECT ;
251+ // [dataformat-xml#484]: in wrap mode, suppress queuing the
252+ // (inner) START_OBJECT here; the wrap state machine queues it
253+ // explicitly after delivering outer START + PROPERTY_NAME.
254+ if (!wrapRoot ) {
255+ _nextToken = JsonToken .START_OBJECT ;
256+ }
220257 break ;
221258 case XmlTokenStream .XML_ROOT_TEXT :
222259 _currText = _xmlTokens .getText ();
@@ -232,6 +269,10 @@ public FromXmlParser(ObjectReadContext readCtxt, IOContext ioCtxt,
232269 _reportError ("Internal problem: invalid starting state (%s)" , _xmlTokens ._currentStateDesc ());
233270 }
234271 }
272+ // [dataformat-xml#484]: activate wrap state machine if feature enabled
273+ if (wrapRoot ) {
274+ _rootWrapStage = WRAP_PENDING_OUTER_START ;
275+ }
235276 }
236277
237278 @ Override
@@ -553,6 +594,14 @@ public JsonToken nextToken() throws JacksonException
553594 _binaryValue = null ;
554595 _numTypesValid = NR_UNKNOWN ;
555596//System.out.println("FromXmlParser.nextToken0: _nextToken = "+_nextToken);
597+ // [dataformat-xml#484]: deliver synthetic root-wrap tokens before normal flow
598+ if (_rootWrapStage != WRAP_INACTIVE ) {
599+ JsonToken wrap = _nextRootWrapToken ();
600+ if (wrap != null ) {
601+ return wrap ;
602+ }
603+ // Otherwise: stage advanced to WRAP_OPEN_OBJECT/SCALAR; fall through to standard logic.
604+ }
556605 if (_nextToken != null ) {
557606 final JsonToken t = _updateToken (_nextToken );
558607 _nextToken = null ;
@@ -583,6 +632,10 @@ public JsonToken nextToken() throws JacksonException
583632 // 13-May-2020, tatu: [dataformat-xml#397]: advance `index` anyway; not
584633 // used for Object contexts, updated automatically by "createChildXxxContext"
585634 _streamReadContext .valueStarted ();
635+ // [dataformat-xml#484]: scalar/null body delivered — queue closing END_OBJECT
636+ if (_rootWrapStage == WRAP_OPEN_SCALAR ) {
637+ _rootWrapStage = WRAP_PENDING_OUTER_END ;
638+ }
586639 }
587640 return t ;
588641 }
@@ -774,13 +827,54 @@ public JsonToken nextToken() throws JacksonException
774827 _nextToken = JsonToken .VALUE_STRING ;
775828 return _updateToken (JsonToken .PROPERTY_NAME );
776829 case XmlTokenStream .XML_END :
830+ // [dataformat-xml#484]: close the synthetic root wrapper before EOF
831+ if (_rootWrapStage == WRAP_OPEN_OBJECT ) {
832+ _rootWrapStage = WRAP_INACTIVE ;
833+ _streamReadContext = _streamReadContext .getParent ();
834+ return _updateToken (JsonToken .END_OBJECT );
835+ }
777836 return _updateTokenToNull ();
778837 default :
779838 return _internalErrorUnknownToken (token );
780839 }
781840 }
782841 }
783842
843+ /**
844+ * [dataformat-xml#484]: deliver the next synthetic root-wrap token, or
845+ * return {@code null} when the wrap state machine has nothing pending and
846+ * the caller should fall through to standard token logic.
847+ *
848+ * @since 3.2
849+ */
850+ private JsonToken _nextRootWrapToken ()
851+ {
852+ switch (_rootWrapStage ) {
853+ case WRAP_PENDING_OUTER_START :
854+ _rootWrapStage = WRAP_PENDING_NAME ;
855+ _streamReadContext = _streamReadContext .createChildObjectContext (-1 , -1 );
856+ return _updateToken (JsonToken .START_OBJECT );
857+ case WRAP_PENDING_NAME :
858+ // Object body case: queue inner START_OBJECT for normal flow to consume.
859+ // Scalar/null body case: _nextToken already holds the value.
860+ if (_nextToken == null ) {
861+ _nextToken = JsonToken .START_OBJECT ;
862+ _rootWrapStage = WRAP_OPEN_OBJECT ;
863+ } else {
864+ _rootWrapStage = WRAP_OPEN_SCALAR ;
865+ }
866+ _streamReadContext .setCurrentName (_xmlTokens .getRootName ().getLocalPart ());
867+ return _updateToken (JsonToken .PROPERTY_NAME );
868+ case WRAP_PENDING_OUTER_END :
869+ _rootWrapStage = WRAP_INACTIVE ;
870+ _streamReadContext = _streamReadContext .getParent ();
871+ return _updateToken (JsonToken .END_OBJECT );
872+ default :
873+ // WRAP_OPEN_OBJECT / WRAP_OPEN_SCALAR — caller should fall through.
874+ return null ;
875+ }
876+ }
877+
784878 /*
785879 /**********************************************************************
786880 /* Overrides of specialized nextXxx() methods
@@ -804,6 +898,11 @@ public String nextName() throws JacksonException {
804898 @ Override
805899 public String nextStringValue () throws JacksonException
806900 {
901+ // [dataformat-xml#484]: wrapper tokens are not String-valued; route
902+ // through nextToken() to keep wrap-state advancement in one place.
903+ if (_rootWrapStage != WRAP_INACTIVE ) {
904+ return (nextToken () == JsonToken .VALUE_STRING ) ? _currText : null ;
905+ }
807906 _binaryValue = null ;
808907 if (_nextToken != null ) {
809908 final JsonToken t = _updateToken (_nextToken );
0 commit comments