azure/union-type-wrappers
Add typed getters and setters over BinaryData properties that represent TypeSpec union types in generated Java models. Use when generated classes expose BinaryData for union-typed fields and you need ergonomic, type-safe accessors instead.
npx skills add https://github.com/Azure/azure-sdk-for-java --skill union-type-wrappers
When the Java codegen encounters a TypeSpec union type (e.g. string | SomeModel), it emits the property as BinaryData. This skill replaces public BinaryData accessors with typed setters and getters for each union variant, following the same pattern established for PromptAgentDefinition.toolChoice.
tsp-location.yaml and pom.xml.TempTypeSpecFiles/. If missing, run tsp-client sync first.Understanding how BinaryData writes to JSON is critical for choosing the correct factory method:
| Factory method | Content type | writeTo(JsonWriter) calls | JSON output |
|---|---|---|---|
| BinaryData.fromString("auto") | StringContent | jsonWriter.writeString("auto") | "auto" (quoted) |
| BinaryData.fromObject("auto") | SerializableContent | jsonWriter.writeRawValue(...) | "auto" (quoted via Jackson) |
| BinaryData.fromObject(42.0) | SerializableContent | jsonWriter.writeRawValue(...) | 42.0 (raw) |
| BinaryData.fromObject(true) | SerializableContent | jsonWriter.writeRawValue(...) | true (raw) |
| BinaryData.fromObject(jsonSerializable) | SerializableContent | jsonWriter.writeRawValue(...) | {...} (JSON object via JacksonAdapter) |
Key: JacksonAdapter has special handling for JsonSerializable types — BinaryData.fromObject() and BinaryData.toObject() both work correctly with Azure JsonSerializable models.
| Union variant | Factory method | Reason |
|---|---|---|
| String value (ID, enum token) | BinaryData.fromString(value) | Writes as JSON string via writeString() |
| Numeric / boolean primitive | BinaryData.fromObject(value) | Writes as raw JSON value |
| Azure JsonSerializable model | BinaryData.fromObject(value) | JacksonAdapter handles serialization |
| Stainless (openai-java) type | BinaryData.fromObject(value) | Jackson handles serialization |
| Union variant | Deserialization | Notes |
|---|---|---|
| String | this.field.toObject(String.class) | Consistent regardless of how BinaryData was created (fromString vs fromObject during deserialization) |
| Primitive (Number, Boolean) | this.field.toObject(Double.class) etc. | Jackson deserializes raw JSON values |
| Azure JsonSerializable model | this.field.toObject(ModelClass.class) | JacksonAdapter calls fromJson() |
| Stainless type | this.field.toObject(StainlessType.class) | Jackson deserializes natively |
Find all BinaryData fields in generated model classes:
grep -rn "private\s\+\(final\s\+\)\?BinaryData\s\+" src/main/java/ --include="*.java"
Exclude List<BinaryData> and Map<..., BinaryData> from this pass — those are collection-of-union patterns that require separate handling.
For each BinaryData property found, determine whether it comes from a union type or an unknown type:
# Search for the property name (use the wire name, e.g. tool_choice, not toolChoice)
grep -rn "<wire_name>" TempTypeSpecFiles/ --include="*.tsp"
type_a | type_b): Proceed with this skill.unknown type: Leave as BinaryData — this is the correct representation.Also check client.tsp for any @@changePropertyType overrides that may have already flattened the union to string (like tool_choice).
For each union type, determine what types the property can hold. Sources:
TempTypeSpecFiles/.OpenAI.*, inspect the Stainless types: jar tf ~/.m2/repository/com/openai/openai-java-core/<version>/openai-java-core-<version>.jar | grep "<TypeName>"
javap -cp <jar> "com.openai.models.responses.<OuterClass>\$<InnerUnion>"
AutoCodeInterpreterToolParam, McpToolFilter).For each union-typed BinaryData property, apply the following pattern:
Do not modify the property declaration. Keep @Generated, the block comment, and the visibility exactly as the codegen produced them. The field is already private — there is nothing to change.
/*
* Original generated block comment.
*/
@Generated
private BinaryData myField;
@Generated.*Internal.// AI Tooling: union type as the first line inside the method body./**
* Get the myField property: original description.
*
* @return the myField value.
*/
BinaryData getMyField() {
// AI Tooling: union type
return this.myField;
}
/**
* Set the myField property: original description.
*
* @param myField the myField value to set.
* @return the MyClass object itself.
*/
MyClass setMyField(BinaryData myField) {
// AI Tooling: union type
this.myField = myField;
return this;
}
For @Immutable classes (value is a constructor param):
BinaryData constructor visibility to package-private (keep for fromJson deserialization).BinaryData getter private./**
* Creates an instance of MyFilter class.
*
* @param type the type value to set.
* @param key the key value to set.
* @param value the value value to set.
*/
MyFilter(MyFilterType type, String key, BinaryData value) {
// AI Tooling: union type
this.type = type;
this.key = key;
this.value = value;
}
public MyFilter(MyFilterType type, String key, String value) {
this.type = type;
this.key = key;
this.value = BinaryData.fromObject(value);
}
Naming convention: set<PropertyName>(<VariantType> value) — use method overloading.
Copy the javadoc from the original generated setter, adapting the @param description to the specific variant type. Add // AI Tooling: union type as the first line inside the method body.
/**
* Set the myField property: original description.
*
* @param myField the string value to set.
* @return the MyClass object itself.
*/
public MyClass setMyField(String myField) {
// AI Tooling: union type
this.myField = BinaryData.fromString(myField);
return this;
}
/**
* Set the myField property: original description.
*
* @param myField the SomeModel value to set.
* @return the MyClass object itself.
*/
public MyClass setMyField(SomeModel myField) {
// AI Tooling: union type
this.myField = BinaryData.fromObject(myField);
return this;
}
When overloading isn't possible (e.g. two different String meanings), disambiguate with parameter names and javadoc.
For List<String> variants:
/**
* Set the allowedTools property: original description.
*
* @param allowedTools the list of tool name strings to set.
* @return the McpTool object itself.
*/
public McpTool setAllowedTools(List<String> allowedTools) {
// AI Tooling: union type
this.allowedTools = BinaryData.fromObject(allowedTools);
return this;
}
Naming convention: get<PropertyName>As<TypeName>()
Copy the javadoc from the original generated getter, adapting the @return description. Add // AI Tooling: union type as the first line inside the method body.
/**
* Get the myField property as a String: original description.
*
* @return the myField value as a String.
*/
public String getMyFieldAsString() {
// AI Tooling: union type
if (this.myField == null) {
return null;
}
return this.myField.toObject(String.class);
}
/**
* Get the myField property as a {@link SomeModel}: original description.
*
* @return the myField value as a SomeModel.
*/
public SomeModel getMyFieldAsSomeModel() {
// AI Tooling: union type
if (this.myField == null) {
return null;
}
return this.myField.toObject(SomeModel.class);
}
For List<String> variants:
/**
* Get the allowedTools property as a list of tool name strings: original description.
*
* @return the allowedTools value as a list of Strings.
*/
@SuppressWarnings("unchecked")
public List<String> getAllowedToolsAsStringList() {
// AI Tooling: union type
if (this.allowedTools == null) {
return null;
}
return this.allowedTools.toObject(List.class);
}
Search for existing code that uses the old BinaryData API:
grep -rn "\.setMyField(BinaryData\|\.getMyField()" src/ --include="*.java"
Update samples, tests, and internal code to use the new typed API. Remove unused BinaryData imports where applicable.
Create a test class per model under src/test/java/.../models/<ModelName>SerializationTests.java.
Each test class must include:
null when the field is not set or absent from JSON.Use these helpers:
private String serializeToJson(MyModel model) throws IOException {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try (JsonWriter jsonWriter = JsonProviders.createWriter(outputStream)) {
model.toJson(jsonWriter);
}
return outputStream.toString("UTF-8");
}
private MyModel deserializeFromJson(String json) throws IOException {
try (JsonReader jsonReader = JsonProviders.createReader(json)) {
return MyModel.fromJson(jsonReader);
}
}
mvn compile -Dbuildhelper.addtestsource.skip=true -Dbuildhelper.addtestresource.skip=true \
-Dcodesnippet.skip=true -Dcheckstyle.skip=true -Dspotless.check.skip=true
mvn "-Dtest=*SerializationTests" test \
-Dcodesnippet.skip=true -Dcheckstyle.skip=true -Dspotless.check.skip=true
All tests must pass before finishing.
Before reporting completion, verify:
BinaryData property was classified as union or unknownBinaryData getter/setter made package-private, name kept, @Generated removed, javadoc preserved// AI Tooling: union type placed inside the body of every modified or added getter/setterget*As*()) with javadoc copied from originalBinaryData imports removed from callers| Symptom | Cause | Fix |
|---------|-------|-----|
| BinaryData.fromObject(jsonSerializable) produces wrong JSON | JacksonAdapter not on classpath | Verify azure-core dependency includes JacksonAdapter |
| toObject(AzureModel.class) fails | JacksonAdapter doesn't find fromJson | Use BinaryData.toObject() which delegates to JacksonAdapter.deserialize() — confirm azure-core ≥ 1.51 |
| Setter creates StringContent but expects raw JSON | Wrong factory method | Use fromString() only for string tokens; use fromObject() for primitives and objects |
| Test fails on deserialized value comparison | Asymmetry between fromString/fromObject for string values | Deserialization always uses fromObject(readUntyped()), producing SerializableContent. Use toObject(String.class) in string getters — never toString() — to normalize both paths. |
| Compilation error: cannot find List | Missing import after adding List<String> setter | Add import java.util.List; |
| @SuppressWarnings needed | Unchecked cast on toObject(List.class) | Add @SuppressWarnings("unchecked") to the method |
Take azure/union-type-wrappers from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.