@@ -217,6 +217,12 @@ public JsonGenerator writeName(String name) throws JacksonException
217217 if (!_inObject ) {
218218 _reportError ("Cannot write a property name: current context not Object but " +_streamWriteContext .typeDesc ());
219219 }
220+ // [dataformats-binary#712] Within a `map`, each "field name" is a map key
221+ // that opens a new entry sub-message
222+ if (_streamWriteContext .inMap ()) {
223+ _startMapEntry (name );
224+ return this ;
225+ }
220226 ProtobufField f = _currField ;
221227 // important: use current field only if NOT repeated field; repeated
222228 // field means an array until START_OBJECT
@@ -249,8 +255,14 @@ public JsonGenerator writeName(SerializableString sstr) throws JacksonException
249255 if (!_inObject ) {
250256 _reportError ("Cannot write a property name: current context not Object but " +_streamWriteContext .typeDesc ());
251257 }
252- ProtobufField f = _currField ;
253258 final String name = sstr .getValue ();
259+ // [dataformats-binary#712] Within a `map`, each "field name" is a map key
260+ // that opens a new entry sub-message
261+ if (_streamWriteContext .inMap ()) {
262+ _startMapEntry (name );
263+ return this ;
264+ }
265+ ProtobufField f = _currField ;
254266 // important: use current field only if NOT repeated field; repeated
255267 // field means an array until START_OBJECT
256268 // NOTE: not ideal -- depends on if it really is sibling field of an array,
@@ -360,6 +372,11 @@ public JsonGenerator writeStartArray() throws JacksonException
360372 if (_currField == null ) { // just a sanity check
361373 return _reportError ("Can not write START_ARRAY without field (message type " +_currMessage .getName ()+")" );
362374 }
375+ // [dataformats-binary#712] A `map` field is also "repeated" underneath, but
376+ // must be written as an Object, not an Array
377+ if (_currField .isMap ) {
378+ _reportError ("Can not write START_ARRAY: field '" +_currField .name +"' is a `map`; write START_OBJECT instead" );
379+ }
363380 if (!_currField .isArray ()) {
364381 _reportError ("Can not write START_ARRAY: field '" +_currField .name +"' not declared as 'repeated'" );
365382 }
@@ -429,6 +446,17 @@ public JsonGenerator writeStartObject() throws JacksonException
429446 }
430447 _currMessage = _schema .getRootType ();
431448 // note: no buffering on root
449+ } else if (_currField .isMap ) {
450+ // [dataformats-binary#712] a `map<K,V>` field: the Object being opened is
451+ // a sequence of entry sub-messages, not a single one. Push a dedicated map
452+ // context; per-entry buffering happens in writeFieldName. No buffering of
453+ // the map as a whole.
454+ _streamWriteContext = _streamWriteContext .createChildMapContext (_currField );
455+ streamWriteConstraints ().validateNestingDepth (_streamWriteContext .getNestingDepth ());
456+ _currMessage = _currField .getMessageType (); // the synthetic entry message
457+ _inObject = true ;
458+ _writeTag = true ;
459+ return this ;
432460 } else {
433461 // but also, field value must be Message if so
434462 if (!_currField .isObject ) {
@@ -464,6 +492,28 @@ public JsonGenerator writeEndObject() throws JacksonException
464492 if (!_inObject ) {
465493 _reportError ("Current context not Object but " +_streamWriteContext .typeDesc ());
466494 }
495+ // [dataformats-binary#712] Closing a `map`: finalize the last open entry (if
496+ // any), then pop -- but do NOT finish-buffer the map as a whole (each entry
497+ // was already length-prefixed on its own).
498+ if (_streamWriteContext .inMap ()) {
499+ if (_streamWriteContext .isEntryOpen ()) {
500+ _streamWriteContext .setEntryOpen (false );
501+ _finishBuffering ();
502+ }
503+ _streamWriteContext = _streamWriteContext .getParent ();
504+ if (_streamWriteContext .inRoot ()) {
505+ if (!_complete ) {
506+ _complete ();
507+ }
508+ } else {
509+ _currMessage = _streamWriteContext .getMessageType ();
510+ }
511+ _currField = _streamWriteContext .getField ();
512+ boolean inObj = _streamWriteContext .inObject ();
513+ _inObject = inObj ;
514+ _writeTag = inObj || !_streamWriteContext .inArray () || !_currField .packed ;
515+ return this ;
516+ }
467517 _streamWriteContext = _streamWriteContext .getParent ();
468518 if (_streamWriteContext .inRoot ()) {
469519 if (!_complete ) {
@@ -1719,6 +1769,141 @@ private final int _writeTag(int ptr)
17191769 return ptr ;
17201770 }
17211771
1772+ /*
1773+ /**********************************************************
1774+ /* Internal map (`map<K,V>`) writes [dataformats-binary#712]
1775+ /**********************************************************
1776+ */
1777+
1778+ /**
1779+ * Opens a new {@code map} entry sub-message for the given key, finalizing the
1780+ * previous entry first if one is still open. Writes the entry's key (tag 1) and
1781+ * leaves {@link #_currField} pointing at the value field (tag 2) so the value
1782+ * write that follows targets it.
1783+ */
1784+ private void _startMapEntry (String name ) throws JacksonException
1785+ {
1786+ final ProtobufField mapField = _streamWriteContext .getField ();
1787+ // Close the previous entry, if any (its length prefix is finalized here)
1788+ if (_streamWriteContext .isEntryOpen ()) {
1789+ _streamWriteContext .setEntryOpen (false );
1790+ _finishBuffering ();
1791+ }
1792+ // Each entry is a length-delimited sub-message, tagged with the map field's tag
1793+ _startBuffering (mapField .typedTag );
1794+ _streamWriteContext .setEntryOpen (true );
1795+ _currMessage = mapField .getMessageType ();
1796+ // Write key (tag 1) ...
1797+ _writeTag = true ;
1798+ _writeMapKey (mapField .getKeyField (), name );
1799+ // ... then prime the value field (tag 2) for the value write that follows
1800+ _currField = mapField .getValueField ();
1801+ _writeTag = true ;
1802+ }
1803+
1804+ /**
1805+ * Writes a map key (always arriving as a {@code String}) using the key field's
1806+ * declared protobuf type. Key types are restricted (and validated during schema
1807+ * resolution) to integral / {@code bool} / {@code string}.
1808+ */
1809+ private void _writeMapKey (ProtobufField keyField , String name ) throws JacksonException
1810+ {
1811+ _currField = keyField ;
1812+ switch (keyField .type ) {
1813+ case STRING :
1814+ writeString (name );
1815+ break ;
1816+ case BOOLEAN :
1817+ writeBoolean (_parseBooleanMapKey (name , keyField ));
1818+ break ;
1819+ case VINT32_Z : // `sint32`: signed only
1820+ writeNumber (_parseIntMapKey (name , keyField , false ));
1821+ break ;
1822+ case VINT32_STD : // `int32` or `uint32`
1823+ case FIXINT32 : // `sfixed32` or `fixed32`
1824+ writeNumber (_parseIntMapKey (name , keyField , true ));
1825+ break ;
1826+ case VINT64_Z : // `sint64`: signed only
1827+ writeNumber (_parseLongMapKey (name , keyField , false ));
1828+ break ;
1829+ case VINT64_STD : // `int64` or `uint64`
1830+ case FIXINT64 : // `sfixed64` or `fixed64`
1831+ writeNumber (_parseLongMapKey (name , keyField , true ));
1832+ break ;
1833+ default :
1834+ // Should not happen: key types are validated during schema resolution
1835+ _reportError ("Unsupported `map` key type " +keyField .type +" for field '" +keyField .name +"'" );
1836+ }
1837+ }
1838+
1839+ /**
1840+ * Parses a 32-bit {@code map} key.
1841+ *<p>
1842+ * {@link FieldType} does not distinguish signed from unsigned declarations
1843+ * ({@code int32} and {@code uint32} are both {@code VINT32_STD}), so for those the
1844+ * accepted range is the union of the two: {@code [Integer.MIN_VALUE, 0xFFFFFFFF]}.
1845+ * Narrowing a value above {@code Integer.MAX_VALUE} keeps the same 32 bits, which is
1846+ * exactly the {@code uint32} / {@code fixed32} encoding.
1847+ *
1848+ * @param allowUnsigned Whether the key type has an unsigned variant; {@code false}
1849+ * for {@code sint32}, which is always signed.
1850+ */
1851+ private int _parseIntMapKey (String name , ProtobufField keyField , boolean allowUnsigned )
1852+ throws JacksonException
1853+ {
1854+ final long l ;
1855+ try {
1856+ l = Long .parseLong (name );
1857+ } catch (NumberFormatException e ) {
1858+ _reportError ("Invalid `map` key \" " +name +"\" for integral key field '" +keyField .name
1859+ +"' (type " +keyField .type +")" );
1860+ return 0 ; // never reached
1861+ }
1862+ final long max = allowUnsigned ? 0xFFFFFFFFL : Integer .MAX_VALUE ;
1863+ if ((l < Integer .MIN_VALUE ) || (l > max )) {
1864+ _reportError ("Invalid `map` key \" " +name +"\" for integral key field '" +keyField .name
1865+ +"' (type " +keyField .type +"): out of range (" +Integer .MIN_VALUE +" to " +max +")" );
1866+ }
1867+ return (int ) l ;
1868+ }
1869+
1870+ /**
1871+ * Parses a 64-bit {@code map} key. As with 32-bit keys the signed and unsigned
1872+ * declarations share a {@link FieldType}, so {@code uint64} keys above
1873+ * {@link Long#MAX_VALUE} are accepted as unsigned: the resulting bit pattern is the
1874+ * same 64-bit varint either way.
1875+ *
1876+ * @param allowUnsigned Whether the key type has an unsigned variant; {@code false}
1877+ * for {@code sint64}, which is always signed.
1878+ */
1879+ private long _parseLongMapKey (String name , ProtobufField keyField , boolean allowUnsigned )
1880+ throws JacksonException
1881+ {
1882+ try {
1883+ return Long .parseLong (name );
1884+ } catch (NumberFormatException e ) {
1885+ if (allowUnsigned ) {
1886+ try {
1887+ return Long .parseUnsignedLong (name );
1888+ } catch (NumberFormatException e2 ) { }
1889+ }
1890+ _reportError ("Invalid `map` key \" " +name +"\" for integral key field '" +keyField .name
1891+ +"' (type " +keyField .type +")" );
1892+ return 0L ; // never reached
1893+ }
1894+ }
1895+
1896+ private boolean _parseBooleanMapKey (String name , ProtobufField keyField ) throws JacksonException {
1897+ if ("true" .equals (name )) {
1898+ return true ;
1899+ }
1900+ if ("false" .equals (name )) {
1901+ return false ;
1902+ }
1903+ _reportError ("Invalid `map` key \" " +name +"\" for boolean key field '" +keyField .name +"'" );
1904+ return false ; // never reached
1905+ }
1906+
17221907 /**
17231908 * Method called when buffering an entry that should be prefixed
17241909 * with a type tag.
0 commit comments