Skip to content

Commit e3135e2

Browse files
authored
fix: Claude Code structured output (#58)
* fix: enhance schema conversion by adding support for properties in _convertSchemaToGemini * fix: adjust properties handling in _convertSchemaToGemini for correct schema conversion * fix: refine handling of metadata keywords in schema conversion * fix: add isProperties parameter to _convertSchemaToGemini for handling property definitions * fix: add nullable property handling in schema conversion
1 parent 0d29e36 commit e3135e2

1 file changed

Lines changed: 48 additions & 6 deletions

File tree

‎src/core/FormatConverter.js‎

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -184,9 +184,10 @@ class FormatConverter {
184184
*
185185
* @param {Object} obj - The schema object to convert
186186
* @param {boolean} [isResponseSchema=false] - If true, applies stricter rules (e.g. anyOf for unions) for Structured Outputs
187+
* @param {boolean} [isProperties=false] - If true, the current object is a map of property definitions, so keys should not be filtered
187188
* @returns {Object} The converted schema
188189
*/
189-
_convertSchemaToGemini(obj, isResponseSchema = false) {
190+
_convertSchemaToGemini(obj, isResponseSchema = false, isProperties = false) {
190191
if (!obj || typeof obj !== "object") return obj;
191192

192193
const result = Array.isArray(obj) ? [] : {};
@@ -211,11 +212,44 @@ class FormatConverter {
211212
unsupportedKeys.push("title", "default", "examples", "$defs", "id");
212213
}
213214

214-
if (unsupportedKeys.includes(key)) {
215+
// ONLY Filter metadata keywords if NOT a property name (isProperties is false)
216+
if (!isProperties && unsupportedKeys.includes(key)) {
215217
continue;
216218
}
217219

218-
if (key === "type") {
220+
// Handle anyOf specially (only when it is a schema keyword)
221+
if (key === "anyOf" && !isProperties) {
222+
if (Array.isArray(obj[key])) {
223+
const variants = obj[key];
224+
const hasNull = variants.some(v => v.type === "null");
225+
const nonNullVariants = variants.filter(v => v.type !== "null");
226+
227+
if (hasNull) {
228+
result.nullable = true;
229+
}
230+
231+
if (nonNullVariants.length === 1) {
232+
// Collapse single variant. Reset isProperties to false for the variant's schema.
233+
const converted = this._convertSchemaToGemini(nonNullVariants[0], isResponseSchema, false);
234+
// Merge converted properties into result
235+
Object.assign(result, converted);
236+
if (hasNull) result.nullable = true;
237+
continue; // Skip setting 'anyOf' explicitly
238+
} else if (nonNullVariants.length > 0) {
239+
// Keep anyOf for multiple variants. Reset isProperties for sub-schemas.
240+
result.anyOf = nonNullVariants.map(v =>
241+
this._convertSchemaToGemini(v, isResponseSchema, false)
242+
);
243+
continue;
244+
} else if (hasNull) {
245+
// Only null type? Keep it as nullable without forcing a specific type.
246+
continue;
247+
}
248+
}
249+
}
250+
251+
// Handle type specially (only when it is a schema keyword)
252+
if (key === "type" && !isProperties) {
219253
if (Array.isArray(obj[key])) {
220254
// Handle nullable types like ["string", "null"]
221255
const types = obj[key];
@@ -247,11 +281,12 @@ class FormatConverter {
247281
// Convert lowercase type to uppercase for Gemini
248282
result[key] = obj[key].toUpperCase();
249283
} else if (typeof obj[key] === "object" && obj[key] !== null) {
250-
result[key] = this._convertSchemaToGemini(obj[key], isResponseSchema);
284+
// Type being an object is a sub-schema definition, not property name mapping
285+
result[key] = this._convertSchemaToGemini(obj[key], isResponseSchema, false);
251286
} else {
252287
result[key] = obj[key];
253288
}
254-
} else if (key === "enum") {
289+
} else if (key === "enum" && !isProperties) {
255290
// 2. Ensure all enum values are strings (Only for Response Schema)
256291
if (isResponseSchema) {
257292
if (Array.isArray(obj[key])) {
@@ -265,7 +300,14 @@ class FormatConverter {
265300
result[key] = obj[key];
266301
}
267302
} else if (typeof obj[key] === "object" && obj[key] !== null) {
268-
result[key] = this._convertSchemaToGemini(obj[key], isResponseSchema);
303+
// Recursion logic:
304+
// - If key is 'properties', next level is a map of property NAMES. Set isProperties = true.
305+
// - Otherwise, if we were currently in a properties map (isProperties is true),
306+
// the value is a schema definition. For its keys, isProperties MUST be false.
307+
const nextIsProperties = key === "properties";
308+
const recursionFlag = isProperties ? false : nextIsProperties;
309+
310+
result[key] = this._convertSchemaToGemini(obj[key], isResponseSchema, recursionFlag);
269311
} else {
270312
result[key] = obj[key];
271313
}

0 commit comments

Comments
 (0)