OpenAPI 3.2 SSE schemas misdescribe unnamed events and literal payloads

Aperta
#12,119 2 commenti 1 reazione 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
55/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
json, openapi

Direzione di ricerca

The bug lives in the TypeSpec OpenAPI 3.2 emitter's SSE stream schema generation. Start by finding the code that produces itemSchema for SSEStream<T> unions, focusing on the paths left unchanged by PR 12021. Add regression tests in the compiler/emitter test harness for unnamed events, text/JSON sentinels, JSON terminal literals, and missing data. Validate output against OpenAPI 3.2 SSE rules and JSON Schema Draft 2020-12.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

bug emitter:openapi3 triaged:core

Problem

OpenAPI 3.2 SSE emission still misdescribes valid streams with unnamed events, literal sentinels, and JSON-encoded terminal literals. These gaps are separate from the named model-valued terminal-event bug in https://github.com/microsoft/typespec/issues/11747.

https://github.com/microsoft/typespec/pull/12021 fixes that named-terminal case and preserves event names for named literal variants. The problems below are in code paths that the PR leaves unchanged.

This affects common contracts such as OpenAI Chat Completions (unnamed JSON chunks followed by a plain-text [DONE] sentinel) and Gemini streamGenerateContent (unnamed JSON records until EOF).

Reproduction

import "@typespec/http";
import "@typespec/events";
import "@typespec/sse";

using TypeSpec.Http;
using TypeSpec.Events;
using TypeSpec.SSE;

model Chunk {
  text: string;
}

@events
union UnnamedEvents {
  Chunk,
}

@route("/unnamed")
@get op unnamed(): SSEStream<UnnamedEvents>;

@events
union ChatEvents {
  Chunk,
  @contentType("text/plain")
  @terminalEvent
  "[DONE]",
}

@route("/chat")
@get op chat(): SSEStream<ChatEvents>;

@events
union JsonTerminalEvents {
  @contentType("application/json")
  @terminalEvent
  "done",
}

@route("/json-terminal")
@get op jsonTerminal(): SSEStream<JsonTerminalEvents>;

@events
union NamedEvents {
  chunk: Chunk,
}

@route("/named")
@get op named(): SSEStream<NamedEvents>;
emit:
  - "@typespec/openapi3"
options:
  "@typespec/openapi3":
    openapi-versions: ["3.2.0"]
1. event is required even for unnamed events

All four emitted itemSchema objects use required: ["event"]. But this is a valid unnamed SSE event:

data: {"text":"hello"}

Its parsed field object, {"data":"{\"text\":\"hello\"}"}, fails validation with must have required property 'event'. An omitted or empty SSE event name defaults to message; the wire field need not exist.

2. Unnamed JSON and sentinel branches overlap

The /chat schema contains:

oneOf:
  - properties:
      data:
        contentMediaType: application/json
        contentSchema:
          $ref: '#/components/schemas/Chunk'
  - properties:
      data:
        contentMediaType: text/plain
        const: '[DONE]'

Even if a parser materializes the default event name, {"event":"message","data":"[DONE]"} fails oneOf: both branches match.

contentMediaType and contentSchema are annotations, not default validation assertions. They do not distinguish the first branch from the sentinel branch. Multiple unnamed model variants have the same problem: different contentSchema annotations alone cannot make their outer oneOf branches exclusive.

3. JSON terminal literals constrain the wrong string

For /json-terminal, the emitter produces:

data:
  contentMediaType: application/json
  const: done

But the explicitly JSON-encoded literal "done" has this wire representation:

data: "done"

The parsed SSE data string includes the JSON quotation marks. With an explicit/default-materialized event: message, that valid JSON data fails the emitted const, whereas unquoted data: done passes.

Terminality should not determine whether a literal is treated as raw text or a JSON-encoded value. The declared payload content type should determine the representation.

4. data is not required

For /named, {"event":"chunk"} passes the emitted schema despite having no data. SSE does not dispatch a message when there are no data lines. OpenAPI 3.2's generic SSE event schema requires data, not event.

Expected

  • Require data for dispatched SSE messages.
  • Require an event name on branches describing named events, not globally when unnamed/default-message variants are present.
  • Preserve the distinction between named events and unnamed/default-message events in mixed unions.
  • Avoid outer oneOf branches whose only differences are non-validating content annotations. For example, an assertion such as not: { const: "[DONE]" } can exclude a plain-text sentinel from the JSON branch; other correct representations are possible.
  • Describe JSON literals inside contentSchema, or constrain their correctly serialized wire string. Do not apply a raw-text const solely because the variant is terminal.
  • Keep event identity and payload representation unchanged when @terminalEvent is added. Representing the disconnect instruction itself is a separate extension/design question.

Useful regression cases include data-only JSON streams, data-only JSON plus a text sentinel, mixed named/default-message events, named text/plain terminal literals, unnamed JSON terminal literals, multiple unnamed model variants, and records missing data.

Verification and references

Reproduced with the existing compiler/emitter test harness and Ajv's Draft 2020-12 validator with default content-keyword behavior. The observations above do not depend on the named model-terminal branch changed in PR 12021.

Lingua principale
Java
Stelle
5.9k
Fork
421
Merge medio
2g 3h
PR unite (30g)
176

Preparare l'ambiente

Apri in Codespaces

Avvia il container di sviluppo del progetto nel browser, con il tuo account GitHub.

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di microsoft/typespec

Tutte le issue di microsoft/typespec

Issue simili

Altre issue su Java

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.