Skip to content
Draft
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 @@ -84,6 +84,7 @@
import com.sun.xml.xsom.XSSimpleType;
import com.sun.xml.xsom.XSTerm;
import com.sun.xml.xsom.XSType;
import com.sun.xml.xsom.XSUnionSimpleType;
import com.sun.xml.xsom.impl.ElementDecl;
import com.sun.xml.xsom.parser.XSOMParser;
import com.sun.xml.xsom.util.DomAnnotationParserFactory;
Expand Down Expand Up @@ -246,12 +247,65 @@ private String processSimpleType(XSSimpleType xs, String elementName) {
} else if (xs.isList()) {
nestingLevel--;
return processSimpleType(xs.asList().getItemType(), null);
} else if (isEnumUnion(xs)) {
createEnumFromUnion(typeName, xs.asUnion());
}

nestingLevel--;
return typeName;
}

private boolean isNonEnumUnion(XSSimpleType xs) {
return xs.isUnion() && !isEnumUnion(xs);
}

private boolean isEnumUnion(XSSimpleType xs) {
if (!xs.isUnion()) {
return false;
}
XSUnionSimpleType unionType = xs.asUnion();
boolean allMembersAreEnums = true;

for (int i = 0; i < unionType.getMemberSize(); i++) {
XSSimpleType member = unionType.getMember(i);
if (!member.isRestriction() || member.getFacet(XSFacet.FACET_ENUMERATION) == null) {
allMembersAreEnums = false;
break;
}
}
return allMembersAreEnums;
}

private void createEnumFromUnion(String typeName, XSUnionSimpleType unionType) {
Type protoType = getType(unionType.getTargetNamespace(), typeName);
if (protoType == null) {
Location location = getLocation(unionType);
List<EnumConstant> constants = new ArrayList<>();
int counter = 1;
Set<String> addedValues = new HashSet<>();

for (int i = 0; i < unionType.getMemberSize(); i++) {
XSRestrictionSimpleType member = unionType.getMember(i).asRestriction();
for (XSFacet facet : member.getDeclaredFacets()) {
String enumValue = facet.getValue().value;
if (addedValues.add(enumValue)) {
String doc = resolveDocumentationAnnotation(facet, false);
List<OptionElement> optionElements = new ArrayList<>();
constants.add(new EnumConstant(location, enumValue, counter++, doc, new Options(Options.ENUM_VALUE_OPTIONS, optionElements)));
}
}
}

List<OptionElement> enumOptionElements = new ArrayList<>();
Options enumOptions = new Options(Options.ENUM_OPTIONS, enumOptionElements);
String doc = resolveDocumentationAnnotation(unionType, false);

ProtoType definedProtoType = ProtoType.get(typeName);
EnumType enumType = new EnumType(definedProtoType, location, doc, typeName, constants, new ArrayList<>(), enumOptions);
addType(unionType.getTargetNamespace(), enumType);
}
Comment on lines +279 to +306

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createEnumFromUnion always adds the generated enum at file scope. If a union-of-enums occurs as a local/anonymous simpleType (e.g., inline on an element), this will create a global enum with a generated name rather than nesting it under the enclosing message (unlike createEnum(...) which supports enclosingType). If local union-of-enums should be supported, consider threading through the enclosing message (or otherwise aligning scoping rules) to avoid polluting the global namespace and to keep naming consistent.

Copilot uses AI. Check for mistakes.
}

private void addField(MessageType message, Field newField) {
addField(message, null, newField);
}
Expand Down Expand Up @@ -502,8 +556,8 @@ public String findFieldType(XSType type) {
XSListSimpleType asList = type.asSimpleType().asList();
XSSimpleType itemType = asList.getItemType();
typeName = itemType.getName();
} else if (type.asSimpleType().isUnion()) {
typeName = DEFAULT_PROTO_PRIMITIVE; // Union always resolves to string
} else if (isNonEnumUnion(type.asSimpleType())) {
typeName = DEFAULT_PROTO_PRIMITIVE; // Non enum union always resolves to string
} else {
typeName = type.asSimpleType().getBaseType().getName();
}
Expand Down Expand Up @@ -711,7 +765,7 @@ private MessageType processComplexType(XSComplexType complexType, String element
}

String name;
if (xsSimpleType.isUnion()) {
if (isNonEnumUnion(xsSimpleType)) {
name = DEFAULT_PROTO_PRIMITIVE;
} else {
name = xsSimpleType.getName();
Expand Down Expand Up @@ -840,7 +894,7 @@ private void processAttribute(MessageType messageType, Set<Object> processedXmlO
XSAttributeDecl decl = attr.getDecl();
XSSimpleType type = decl.getType();

if (type.getPrimitiveType() != null || type.isList() || type.isUnion()) {
if (type.getPrimitiveType() != null || type.isList() || isNonEnumUnion(type)) {
String fieldName = decl.getName();
String doc = resolveDocumentationAnnotation(decl, false);
int tag = messageType.getNextFieldNum();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,12 @@ public void testUnion() throws IOException {
compareExpectedAndGenerated(expectedRootFolder, "default/union.proto", generatedRootFolder, "default/default.proto");
}

@Test
public void testUnionOfEnums() throws IOException {
generateProtobufNoOptions("basic/union-of-enums.xsd");
compareExpectedAndGenerated(expectedRootFolder, "default/union-of-enums.proto", generatedRootFolder, "default/default.proto");
}
Comment on lines +182 to +186

Copilot AI Apr 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test only asserts that a union-of-enums simpleType produces an enum declaration, but it doesn’t cover the key behavior of using that enum when the union type is referenced by an element/attribute/simpleContent. Adding an XSD fixture where UnionEnumeration is used as a field type would help prevent regressions where unions still resolve to string.

Copilot uses AI. Check for mistakes.

// @Test
public void testValidationRules() throws IOException {
generateProtobufNoOptions("basic/validationrules.xsd");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// default.proto at 0:0
syntax = "proto3";
package default;

enum Enum1 {
// Default
ENUM1_UNSPECIFIED = 0;
ENUM1_E_1_VAL_1 = 1;
ENUM1_E_1_VAL_2 = 2;
}

enum Enum2 {
// Default
ENUM2_UNSPECIFIED = 0;
ENUM2_E_2_VAL_1 = 1;
ENUM2_E_2_VAL_2 = 2;
}

enum UnionEnumeration {
// Default
UNION_ENUMERATION_UNSPECIFIED = 0;
UNION_ENUMERATION_E_1_VAL_1 = 1;
UNION_ENUMERATION_E_1_VAL_2 = 2;
UNION_ENUMERATION_E_2_VAL_1 = 3;
UNION_ENUMERATION_E_2_VAL_2 = 4;
}
23 changes: 23 additions & 0 deletions schema2proto-lib/src/test/resources/xsd/basic/union-of-enums.xsd
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>

<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema" version="1.0">

<xsd:simpleType name="UnionEnumeration">
<xsd:union memberTypes="Enum1 Enum2"/>
</xsd:simpleType>

<xsd:simpleType name="Enum1">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="e1val1"/>
<xsd:enumeration value="e1val2"/>
</xsd:restriction>
</xsd:simpleType>

<xsd:simpleType name="Enum2">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="e2val1"/>
<xsd:enumeration value="e2val2"/>
</xsd:restriction>
</xsd:simpleType>

</xsd:schema>
Loading