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.
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.
Union Type Wrappers for Generated Java Models
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.
Preconditions
You must be in a Java SDK module directory containing tsp-location.yaml and pom.xml.
TypeSpec sources must be available in 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.
Setter factory method rules
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
Getter deserialization rules
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
Workflow
1. Scan for BinaryData properties
Find all BinaryData fields in generated model classes:
Exclude List<BinaryData> and Map<..., BinaryData> from this pass — those are collection-of-union patterns that require separate handling.
2. Cross-reference against TypeSpec
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"
Union type (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).
3. Identify the union variants
For each union type, determine what types the property can hold. Sources:
Local TSP files — look at the type definition in TempTypeSpecFiles/.
Stainless SDK JAR — if the union comes from 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>"
Generated Azure models — check if the Azure SDK already generates the variant model classes (e.g. AutoCodeInterpreterToolParam, McpToolFilter).
4. Apply changes to the model class
For each union-typed BinaryData property, apply the following pattern:
4a. Leave the property field as-is
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.
*/@Generatedprivate BinaryData myField;
4b. Make the existing getter and setter package-private
Remove @Generated.
Change visibility to package-private (no access modifier). This keeps them hidden from SDK consumers but accessible to unit tests in the same package.
Keep the original method name — do NOT rename to *Internal.
Keep the original javadoc intact.
Add // 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 typereturnthis.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 typethis.myField = myField;
returnthis;
}
For @Immutable classes (value is a constructor param):
Change the BinaryData constructor visibility to package-private (keep for fromJson deserialization).
Add public constructor overloads for each variant.
Make the 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 typethis.type = type;
this.key = key;
this.value = value;
}
publicMyFilter(MyFilterType type, String key, String value) {
this.type = type;
this.key = key;
this.value = BinaryData.fromObject(value);
}
4c. Add typed setters (one per union variant)
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 typethis.myField = BinaryData.fromString(myField);
returnthis;
}
/**
* 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 typethis.myField = BinaryData.fromObject(myField);
returnthis;
}
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 typethis.allowedTools = BinaryData.fromObject(allowedTools);
returnthis;
}
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 typeif (this.myField == null) {
returnnull;
}
returnthis.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 typeif (this.myField == null) {
returnnull;
}
returnthis.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 typeif (this.allowedTools == null) {
returnnull;
}
returnthis.allowedTools.toObject(List.class);
}
5. Update callers
Search for existing code that uses the old BinaryData API:
Update samples, tests, and internal code to use the new typed API. Remove unused BinaryData imports where applicable.
6. Write unit tests
Create a test class per model under src/test/java/.../models/<ModelName>SerializationTests.java.
Each test class must include:
Serialization tests — one per union variant. Construct the model with the typed setter, serialize to JSON, assert the JSON contains the expected field and value.
Deserialization tests — one per union variant. Parse a JSON string, assert the typed getter returns the correct value.
Null/absent tests — verify getters return null when the field is not set or absent from JSON.
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.