Skip to content

StructuredOutput drops additionalProperties (map) fields on Gemini; Ajv accepts the lossy {} #139

Closed

Description

@byapparov

Context

In aictrl prod on 2026-10-10, all 5 PR Explanation runs on Gemini 3.8 Flash (Vertex) stored diagrams[*].elements as {}. In each run the model had written and linted a result file with populated elements ("lint: zero errors" right before the StructuredOutput call). GLM 5.2 runs were unaffected.

Root cause

@ai-sdk/google 2.0.54 convertJSONSchemaToOpenAPISchema keeps only type, description, required, properties, items, allOf/anyOf/oneOf, format, const, minLength and enum. It drops additionalProperties, so a map-typed field (elements: {type: object, additionalProperties: {...}}) reaches Gemini as a bare {type: object}, and constrained decoding emits {}. The CLI's Ajv check then accepts {} (valid against the original schema), and --output-result writes the lossy copy.

Expected

Map-typed fields survive the provider schema conversion, or the CLI fails fast when a schema uses a construct the provider path can't express. It must never silently empty a field.

Acceptance Criteria

  • A test with a schema containing additionalProperties (object map) round-trips non-empty data on the Google provider path, or the run fails with a clear config error.
  • Options to weigh: pass the full JSON Schema via Gemini's parametersJsonSchema / responseJsonSchema; or skip provider-side schema constraints for StructuredOutput and rely on Ajv plus the corrective turn.

References

aictrl-dev/aictrl#6172 (symptom), aictrl run 2026-10-10 executions e9201d87, e6763c4b, 7b0e8945, 81c91038, 39ba301b. Related: #5958 / #129 (schema mode).

Activity

  1. byapparov commented on Oct 10, 2026

    @byapparov
    ContributorAuthor

    Reproduction on aictrl run 0.4.7 (the prod executor pin), 2026-10-10

    Confirmed: the map-typed field reaches Gemini without additionalProperties, and Gemini returns {}.

    Files. Put expected.json in an empty git dir ws/.

    schema-nomin.json

    {
      "type": "object",
      "required": [
        "title",
        "elements"
      ],
      "properties": {
        "title": {
          "type": "string"
        },
        "elements": {
          "type": "object",
          "additionalProperties": {
            "type": "object",
            "required": [
              "label"
            ],
            "properties": {
              "label": {
                "type": "string"
              },
              "lines": {
                "type": "array",
                "items": {
                  "type": "integer"
                }
              }
            }
          }
        }
      }
    }

    expected.json

    {
      "title": "Task lifecycle",
      "elements": {
        "queued->running": { "label": "dispatch", "lines": [12, 40] },
        "[*]->queued": { "label": "create", "lines": [3] },
        "TASKS.org_id": { "label": "org_id uuid FK", "lines": [88, 89, 90] },
        "1": { "label": "first step", "lines": [] },
        "A": { "label": "Actor A", "lines": [7] },
        "done->[*]": { "label": "finish", "lines": [101] }
      }
    }

    prompt.txt

    Write the JSON in expected.json to result.json unchanged, then call StructuredOutput with exactly that object.
    

    Command

    aictrl run --model google-vertex/gemini-3.8-flash --format json --dir ws \
      --output-schema schema-nomin.json --output-result out.json < prompt.txt
    

    Result: ws/result.json is identical to expected.json (all 6 keys), but out.json has "elements": {} and the exit code is 0.

    On the wire (a pass-through proxy logged the Vertex request): the StructuredOutput function declaration carries "elements":{"type":"object"} with toolConfig.functionCallingConfig.mode: ANY. Both additionalProperties and minProperties are gone. Offline, @ai-sdk/google 2.0.54's convertJSONSchemaToOpenAPISchema on the same schema gives:

    {"required":["title","elements"],"type":"object","properties":{"title":{"type":"string"},"elements":{"type":"object"}}}

    The 0.4.7 bundle contains two copies of that converter, and neither handles additionalProperties.

    Matrix

    Runs Model / schema StructuredOutput elements Published Ajv Exit
    N1–N2 Gemini 3.8 Flash, map schema (no minProperties, prod-like) {} 2/2 {} accepted 0 (silent loss)
    V1–V5 Gemini 3.8 Flash, map + minProperties: 1 {} 5/5 none rejected 3
    G1–G3 GLM 5.2 (zai-coding-plan/glm-5.2), map 6/6 keys identical accepted 0
    A1–A3 Gemini 3.8 Flash, elements as an array of {id,label,lines} 6/6 identical (key order only) accepted 0

    In every run the file the model wrote matched expected.json exactly. The loss is only in the schema-constrained StructuredOutput call.

    Workaround evidence: array-shaped collections survive the converter (full items kept; only minItems is dropped).

  2. byapparov commented on Oct 10, 2026

    @byapparov
    ContributorAuthor

    Upstream status: fixed in @ai-sdk/google 4.0.70 (vercel/ai#20757), which preserves the JSON Schema via parametersJsonSchema/responseJsonSchema. That is the AI SDK v7 line only; the v6 line (google 3.0.131) still converts, and upstream OpenCode dev (ai 6.0.168, google 3.0.73) has this bug too. Decision: fix via the SDK migration, tracked in #141.

  3. byapparov commented on Oct 10, 2026

    @byapparov
    ContributorAuthor

    Blocked by #141 (AI SDK v7 migration). Keep this open until #141 merges and the reproduction above passes on Gemini (5/5, all keys published). #140 (publish the validated file) would stop PR Explanations from hitting this, but any inline map-typed --output-schema stays affected until #141.

  4. byapparov commented on Oct 10, 2026

    @byapparov
    ContributorAuthor

    Update: on AI SDK v7 (probe in #141), the map field is no longer emptied, but Gemini rewrites non-identifier keys in its function-call response (queued->running→queued__running, 1→_1, TASKS.org_id→TASKS_org_id), and that still passes Ajv silently. So the acceptance criterion "all 6 keys published" fails on v7 alone. #140 (path form) avoids it for file-based results; for inline schema results, map keys with non-identifier characters are unsafe on Gemini.

  5. byapparov commented on Oct 11, 2026

    @byapparov
    ContributorAuthor

    Resolved for StructuredOutput by #142 (0.4.8): in schema mode the tool's only argument is {path}, so no map-typed result schema reaches the Google converter. Verified in prod on 2026-10-10: Talentrix (Gemini) PR Explanations store all diagram elements. The underlying converter limitation in @ai-sdk/google 2.x/3.x still applies to any other map-typed tool schema sent to Gemini (e.g. an MCP tool). If that shows up, reopen this together with #141.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions