Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@ private FieldType(int wt, DataType.ScalarType... aliases) {

public int getWireType() { return _wireType; }

/**
* Whether fields of this type may use "packed" encoding when repeated
* (per protobuf spec, only scalar numeric/enum/boolean types can be packed;
* length-delimited types like String/Bytes/Message cannot).
*/
public boolean isPackable() {
return _wireType != WireType.LENGTH_PREFIXED;
}

public boolean usesZigZag() {
return (this == VINT32_Z) || (this == VINT64_Z);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,14 @@ public static class FileDescriptorProto

public ProtoFile.Syntax getSyntax()
{
if (syntax == null) {
return ProtoFile.Syntax.PROTO_2;
// 01-Jul-2026, tatu: [dataformats-binary#134] Real `FileDescriptorProto.syntax`
// values (as written by protoc) are lowercase ("proto2"/"proto3"), but the
// enum constants are PROTO_2/PROTO_3: `valueOf(syntax)` would throw for any
// proto3-origin descriptor set.
if ("proto3".equals(syntax)) {
return ProtoFile.Syntax.PROTO_3;
}
return ProtoFile.Syntax.valueOf(syntax);
return ProtoFile.Syntax.PROTO_2;
}

public void setPackage(String p) { _package = p; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,27 @@ public class NativeProtobufSchema
protected final String _name;
protected final Collection<TypeElement> _nativeTypes;

/**
* @since 2.21.5 [dataformats-binary#134]
*/
protected final boolean _isProto3;

protected volatile String[] _messageNames;

protected NativeProtobufSchema(ProtoFile input)
{
this(input.filePath(), input.typeElements());
this(input.filePath(), input.typeElements(), input.syntax() == ProtoFile.Syntax.PROTO_3);
}

protected NativeProtobufSchema(String name, Collection<TypeElement> types) {
this(name, types, false);
}

protected NativeProtobufSchema(String name, Collection<TypeElement> types)
protected NativeProtobufSchema(String name, Collection<TypeElement> types, boolean isProto3)
{
_name = name;
_nativeTypes = types;
_isProto3 = isProto3;
}

public static NativeProtobufSchema construct(ProtoFile input) {
Expand Down Expand Up @@ -64,7 +74,7 @@ public ProtobufSchema forType(String messageTypeName)
+"') has no message type with name '"+messageTypeName+"': known types: "
+getMessageNames());
}
return new ProtobufSchema(this, TypeResolver.resolve(_nativeTypes, msg));
return new ProtobufSchema(this, TypeResolver.resolve(_nativeTypes, msg, _isProto3));
}

/**
Expand All @@ -78,7 +88,7 @@ public ProtobufSchema forFirstType()
throw new IllegalArgumentException("Protobuf schema definition (name '"+_name
+"') contains no message type definitions");
}
return new ProtobufSchema(this, TypeResolver.resolve(_nativeTypes, msg));
return new ProtobufSchema(this, TypeResolver.resolve(_nativeTypes, msg, _isProto3));
}

public List<String> getMessageNames() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,23 +59,36 @@ public class ProtobufField
public final boolean isStdEnum;

public ProtobufField(FieldElement nativeField, FieldType type) {
this(nativeField, type, null, null);
this(nativeField, type, false);
}

/**
* @param isProto3 Whether enclosing schema uses proto3 syntax: affects default
* "packed" setting for repeated scalar fields when not explicitly specified
* (proto3 defaults to packed, proto2 to unpacked)
*/
public ProtobufField(FieldElement nativeField, FieldType type, boolean isProto3) {
this(nativeField, type, null, null, isProto3);
}

public ProtobufField(FieldElement nativeField, ProtobufMessage msg) {
this(nativeField, FieldType.MESSAGE, msg, null);
this(nativeField, FieldType.MESSAGE, msg, null, false);
}

public ProtobufField(FieldElement nativeField, ProtobufEnum et) {
this(nativeField, FieldType.ENUM, null, et);
this(nativeField, FieldType.ENUM, null, et, false);
}

public ProtobufField(FieldElement nativeField, ProtobufEnum et, boolean isProto3) {
this(nativeField, FieldType.ENUM, null, et, isProto3);
}

public static ProtobufField unknownField() {
return new ProtobufField(null, FieldType.MESSAGE, null, null);
return new ProtobufField(null, FieldType.MESSAGE, null, null, false);
}

protected ProtobufField(FieldElement nativeField, FieldType type,
ProtobufMessage msg, ProtobufEnum et)
ProtobufMessage msg, ProtobufEnum et, boolean isProto3)
{
this.type = type;
wireType = type.getWireType();
Expand Down Expand Up @@ -113,8 +126,15 @@ protected ProtobufField(FieldElement nativeField, FieldType type,
* we can't use 'isPacked()' in 3.1.5 (and probably deprecated has same issue);
* let's add a temporary workaround.
*/
packed = _findBooleanOption(nativeField, "packed");
deprecated = _findBooleanOption(nativeField, "deprecated");
Boolean explicitPacked = _findBooleanOptionValue(nativeField, "packed");
if (explicitPacked != null) {
packed = explicitPacked.booleanValue();
} else {
// 01-Jul-2026: [dataformats-binary#134] proto3 defaults repeated
// scalar/enum fields to packed encoding unless overridden
packed = repeated && isProto3 && type.isPackable();
}
deprecated = Boolean.TRUE.equals(_findBooleanOptionValue(nativeField, "deprecated"));

// 13-Apr-2017, tatu: [databind#79] Need to write length-prefixed for packed arrays
if (repeated && packed) {
Expand All @@ -127,18 +147,18 @@ protected ProtobufField(FieldElement nativeField, FieldType type,
isObject = (type == FieldType.MESSAGE);
}

private static boolean _findBooleanOption(FieldElement f, String key)
private static Boolean _findBooleanOptionValue(FieldElement f, String key)
{
for (OptionElement opt : f.options()) {
if (key.equals(opt.name())) {
Object val = opt.value();
if (val instanceof Boolean) {
return ((Boolean) val).booleanValue();
return (Boolean) val;
}
return "true".equals(String.valueOf(val).trim());
return Boolean.valueOf("true".equals(String.valueOf(val).trim()));
}
}
return false;
return null;
}

public void assignMessageType(ProtobufMessage msgType) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,21 @@ public class TypeResolver
*/
private Map<String,ProtobufMessage> _resolvedMessageTypes;

/**
* Whether enclosing schema (single .proto file) uses proto3 syntax: affects
* default "packed" setting for repeated scalar/enum fields.
*
* @since 2.21.5 [dataformats-binary#134]
*/
private final boolean _isProto3;

protected TypeResolver(TypeResolver p, String name, Map<String,MessageElement> declaredMsgs,
Map<String,ProtobufEnum> enums)
Map<String,ProtobufEnum> enums, boolean isProto3)
{
_parent = p;
_contextName = name;
_enumTypes = enums;
_isProto3 = isProto3;
if (declaredMsgs == null) {
declaredMsgs = Collections.emptyMap();
}
Expand All @@ -58,21 +67,29 @@ protected TypeResolver(TypeResolver p, String name, Map<String,MessageElement> d
* types it depends on.
*/
public static ProtobufMessage resolve(Collection<TypeElement> nativeTypes, MessageElement rawType) {
final TypeResolver rootR = construct(null, null, nativeTypes);
return resolve(nativeTypes, rawType, false);
}

/**
* @since 2.21.5 [dataformats-binary#134]
*/
public static ProtobufMessage resolve(Collection<TypeElement> nativeTypes, MessageElement rawType,
boolean isProto3) {
final TypeResolver rootR = construct(null, null, nativeTypes, isProto3);
// Important: parent context for "root types", but child context for nested; further,
// resolution happens in "child" context to allow proper referencing
return TypeResolver.construct(rootR, rawType.name(), rawType.nestedElements())
return TypeResolver.construct(rootR, rawType.name(), rawType.nestedElements(), isProto3)
._resolve(rawType);
}

protected ProtobufMessage resolve(TypeResolver parent, MessageElement rawType)
{
return TypeResolver.construct(this, rawType.name(), rawType.nestedElements())
return TypeResolver.construct(this, rawType.name(), rawType.nestedElements(), _isProto3)
._resolve(rawType);
}

protected static TypeResolver construct(TypeResolver parent, String localName,
Collection<TypeElement> nativeTypes)
Collection<TypeElement> nativeTypes, boolean isProto3)
{
Map<String,MessageElement> declaredMsgs = null;
Map<String,ProtobufEnum> declaredEnums = new LinkedHashMap<>();
Expand All @@ -92,7 +109,7 @@ protected static TypeResolver construct(TypeResolver parent, String localName,
}
} // no other known types?
}
return new TypeResolver(parent, localName, declaredMsgs, declaredEnums);
return new TypeResolver(parent, localName, declaredMsgs, declaredEnums, isProto3);
}

protected void addEnumType(String name, ProtobufEnum enumType) {
Expand Down Expand Up @@ -125,6 +142,17 @@ protected static ProtobufEnum constructEnum(EnumElement nativeEnum)
protected ProtobufMessage _resolve(MessageElement rawType)
{
List<FieldElement> rawFields = rawType.fields();
List<OneOfElement> oneOfs = rawType.oneOfs();
// 01-Jul-2026, tatu: [dataformats-binary#134] Fields declared inside a
// `oneof` block live in a separate list from regular fields and were
// silently dropped during resolution; merge them in so they're not lost.
if (!oneOfs.isEmpty()) {
List<FieldElement> merged = new ArrayList<FieldElement>(rawFields);
for (OneOfElement oneOf : oneOfs) {
merged.addAll(oneOf.fields());
}
rawFields = merged;
}
ProtobufField[] resolvedFields = new ProtobufField[rawFields.size()];

ProtobufMessage message = new ProtobufMessage(rawType.name(), resolvedFields);
Expand All @@ -142,7 +170,7 @@ protected ProtobufMessage _resolve(MessageElement rawType)
ProtobufField pbf;

if (type != null) { // simple type
pbf = new ProtobufField(f, type);
pbf = new ProtobufField(f, type, _isProto3);
} else if (fieldType instanceof DataType.NamedType) {
final String typeStr = ((DataType.NamedType) fieldType).name();

Expand Down Expand Up @@ -260,7 +288,7 @@ private ProtobufField _findLocalResolved(FieldElement nativeField, String typeSt
}
ProtobufEnum et = _enumTypes.get(typeStr);
if (et != null) {
return new ProtobufField(nativeField, et);
return new ProtobufField(nativeField, et, _isProto3);
}
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package com.fasterxml.jackson.dataformat.protobuf;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.fasterxml.jackson.dataformat.protobuf.schema.ProtobufSchema;
import com.fasterxml.jackson.dataformat.protobuf.schema.ProtobufSchemaLoader;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

// [dataformats-binary#134]: fields declared inside a `oneof` block live in a
// separate list (`MessageElement.oneOfs()`) from regular fields
// (`MessageElement.fields()`); TypeResolver only ever resolved the latter,
// so `oneof` member fields were silently dropped from the schema -- no
// error, they simply couldn't be read or written.
public class OneofFieldResolutionTest extends ProtobufTestBase
{
private final ProtobufMapper MAPPER = newObjectMapper();

private final static String SCHEMA_STR = "message t {\n"
+ " oneof choice {\n"
+ " string a = 1;\n"
+ " int32 b = 2;\n"
+ " }\n"
+ " optional string other = 3;\n"
+ "}\n";

@Test
public void testOneofFieldsAreResolved() throws Exception
{
ProtobufSchema schema = ProtobufSchemaLoader.std.parse(SCHEMA_STR);

assertEquals(3, schema.getRootType().getFieldCount());
assertNotNull(schema.getRootType().field("a"));
assertNotNull(schema.getRootType().field("b"));
assertNotNull(schema.getRootType().field("other"));
}

@Test
public void testOneofFieldsRoundTrip() throws Exception
{
ProtobufSchema schema = ProtobufSchemaLoader.std.parse(SCHEMA_STR);

ObjectNode input = MAPPER.createObjectNode();
input.put("a", "value-for-a");
input.put("other", "other-value");

byte[] bytes = MAPPER.writer(schema).writeValueAsBytes(input);
JsonNode result = MAPPER.readerFor(JsonNode.class).with(schema).readValue(bytes);

assertEquals("value-for-a", result.get("a").asText());
assertEquals("other-value", result.get("other").asText());
}
}
Loading