You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Feature Request] Support structured outputs (JSON Schema) in OpenAIChatCompletion #2700
Add support for OpenAI's structured outputs (response_format: {"type": "json_schema", ...}) to OpenAIChatCompletion in both Python and Scala.
Currently, OpenAIChatCompletion returns stringified/unstructured output or standard JSON mode ({"type": "json_object"}). Introducing structured outputs ensures that LLM responses strictly conform to a provided JSON schema or a Spark StructType.
Motivation & Use Case
When performing distributed batch inference across large datasets in Spark:
LLM responses can deviate from expected formatting, breaking downstream transformations.
Users are forced to write custom PySpark UDFs using json.loads or Pydantic for schema validation, which adds serialization overhead and fails when the model outputs malformed JSON.
Native structured output support guarantees 100% adherence to schema, enabling zero-shot deterministic column extraction directly into the DataFrame.
Proposed Solution / API Design
Expose a setter method such as .setResponseFormat() or .setResponseSchema() that accepts a JSON schema string or a Spark StructType:
Hey Shresth Kumar Gupta (@Srazyy) 👋!
Thank you so much for reporting the issue/feature request 🚨.
Someone from SynapseML Team will be looking to triage this issue soon.
We appreciate your patience.
The core structured-output support already exists in SynapseML 1.1.0 and later. It was added in #2428 and refined in #2436. OpenAIChatCompletion.setResponseFormat accepts a Python dictionary or Scala Map[String, Any], including type: "json_schema" and strict: true.
There are two separate steps:
Use setResponseFormat to request schema-constrained JSON from the model.
Use Spark's built-in from_json to turn the returned JSON text into a typed struct. This does not require a Python UDF or per-row json.loads.
Python
This example assumes an existing Spark session named spark and matching SynapseML Python/JVM packages. Configure AZURE_OPENAI_API_KEY, AZURE_OPENAI_SERVICE_NAME, and AZURE_OPENAI_DEPLOYMENT_NAME in your environment. The service name is the Azure OpenAI resource name, not its full URL. Choose a deployment whose model supports structured outputs.
The equivalent Scala example uses the same environment variables and an existing spark session:
importcom.microsoft.azure.synapse.ml.services.openai.OpenAIChatCompletionimportorg.apache.spark.sql.functions._importorg.apache.spark.sql.types._importspark.implicits._valschema:Map[String, Any] =Map(
"type"->"object",
"properties"->Map(
"sentiment"->Map("type"->"string"),
"score"->Map("type"->"integer")
),
"required"->Seq("sentiment", "score"),
"additionalProperties"->false
)
valresponseFormat:Map[String, Any] =Map(
"type"->"json_schema",
"json_schema"->Map(
"name"->"sentiment_schema",
"strict"->true,
"schema"-> schema
)
)
valmessagesDf=Seq(
"Extract sentiment and an integer score from 0 to 10: I love this product."
).toDF("input_text").withColumn(
"messages",
array(struct(lit("user").as("role"), col("input_text").as("content")))
)
valchat=newOpenAIChatCompletion()
.setSubscriptionKey(sys.env("AZURE_OPENAI_API_KEY"))
.setCustomServiceName(sys.env("AZURE_OPENAI_SERVICE_NAME"))
.setDeploymentName(sys.env("AZURE_OPENAI_DEPLOYMENT_NAME"))
.setMessagesCol("messages")
.setResponseFormat(responseFormat)
.setOutputCol("response")
.setErrorCol("error")
valsparkSchema=newStructType()
.add("sentiment", StringType)
.add("score", IntegerType)
valresult= chat.transform(messagesDf).withColumn(
"extracted_data",
from_json(col("response.choices").getItem(0).getField("message").getField("content"), sparkSchema)
)
result.select(
col("extracted_data.*"),
col("error"),
col("response.choices").getItem(0).getField("finish_reason").as("finish_reason")
).show(false)
What remains different from the proposed API
OpenAIChatCompletion takes setMessagesCol, not setPromptCol, and keeps the response envelope. The JSON text is in choices[0].message.content.
There is currently no setResponseSchema method or direct conversion from a Spark StructType to an OpenAI JSON Schema. The StructType above describes the Spark parsing step, not the model request.
Schema input to setResponseFormat must be a dictionary/Scala map, not a JSON-encoded string. If your schema starts as a JSON string, parse it once on the driver before configuring the transformer.
For prompt templates and built-in typed output, OpenAIPrompt also supports setResponseFormat, setPromptTemplate, setPostProcessing("json"), and setPostProcessingOptions({"jsonSchema": "sentiment STRING, score INT"}) in Python.
So the JSON Schema request support and UDF-free typed extraction are already available. Direct StructType conversion or a setResponseSchema convenience method would be separate enhancements.
I checked the released source and validated the published SynapseML 1.1.3 Python package and JVM jars on Spark 3.5.0. Thirteen focused tests passed with no skips, including four mock HTTP requests checking the exact strict-schema payload, typed extraction, OpenAIPrompt JSON post-processing, rejected input forms, and save/load. A separately compiled Scala check also passed. These were local mock-service checks, not live-model inference.
Strict schema support does not eliminate service errors, refusals, or truncated responses, and it does not guarantee that extracted values are factually correct. Check error, finish_reason, and the returned content before consuming a result.
If your runtime includes an older SynapseML version, make sure both the Python package and the JVM jars are updated together.
The existing setResponseFormat API supports structured outputs, as described above. We can make the experience simpler by accepting the inner JSON Schema directly and adding the API-specific response-format envelope automatically. I've opened PR #2708 for this improvement: #2708.
The proposed setResponseSchema helper works with OpenAIPrompt, OpenAIChatCompletion, and OpenAIResponses, in both Python and Scala. Its defaults are name="response_schema" and strict=True.
These examples require a build containing PR #2708. The new setter is not in SynapseML 1.1.3. Existing released versions can continue using the full-envelope setResponseFormat examples in the earlier reply.
By "partial schema," we mean the inner JSON Schema without the outer response-format metadata. The helper preserves that schema; it does not infer required, add additionalProperties, convert a Spark StructType, or fill in missing schema definitions. For strict output, supply a schema supported by the selected model.
Python
Run in a configured Spark session. Set AZURE_OPENAI_ENDPOINT to the resource root, such as https://<resource>.openai.azure.com, and supply AZURE_OPENAI_API_KEY and AZURE_OPENAI_DEPLOYMENT through your environment or secret-management system. The deployment must support structured outputs and the selected API.
OpenAIResponses also uses setMessagesCol. The JSON text is in an output_text content item. This expression finds that item without assuming that the first output entry contains text:
responses= (
OpenAIResponses()
.setUrl(endpoint)
.setSubscriptionKey(key)
.setDeploymentName(deployment)
.setMessagesCol("messages")
.setResponseSchema(schema)
.setOutputCol("result")
.setErrorCol("error")
)
response_text=F.expr(
"try_element_at(filter(flatten(transform(""filter(result.output, x -> x.content IS NOT NULL), x -> x.content)), ""x -> x.type = 'output_text'), 1).text"
)
responses.transform(messages).select(
"text",
F.from_json(response_text, result_schema).alias("parsed"),
"error",
).show(truncate=False)
To customize the metadata on any of these stages, use .setResponseSchema(schema, name="review_schema", strict=False). Omitting those arguments requests the default name and strict output.
Scala equivalents
Scala
The same three APIs accept a Map[String, Any]. Use ListMap where JSON property order matters.
Shared setup, using the same environment variables:
valresponses=newOpenAIResponses()
.setUrl(endpoint)
.setSubscriptionKey(key)
.setDeploymentName(deployment)
.setMessagesCol("messages")
.setResponseSchema(schema)
.setOutputCol("result")
.setErrorCol("error")
valresponseText= expr(
"try_element_at(filter(flatten(transform("+"filter(result.output, x -> x.content IS NOT NULL), x -> x.content)), "+"x -> x.type = 'output_text'), 1).text"
)
responses.transform(messages).select(
col("text"), from_json(responseText, resultType).as("parsed"), col("error")
).show(truncate =false)
Scala also supports .setResponseSchema(schema, strict = false) and .setResponseSchema(schema, name = "review_schema", strict = false).
These examples use Spark-native parsing rather than Python UDFs. Applications should inspect the error column and handle null parsed results, refusals, and incomplete responses; strict output is not a guarantee that every request produces a usable result.
The exact Python and Scala examples above passed local Spark execution against the PR's generated wrappers and JVM classes, with loopback HTTP checks for request formats and typed results. The updated notebook adds eight live-service cases covering defaults and custom non-strict options across the four routes, including both OpenAIPrompt modes.
Live validation passed on the final head: the updated OpenAI.ipynb passed Databricks CI on commit 10bead3b18, executing all eight new-option cases. This is actual Azure inference, not just configuration or loopback validation. Final CI checks are green, with all 65 jobs passing. This does not claim Fabric runtime validation; the fork PR's pipeline excludes Fabric E2E.
Static-schema caching and format compatibility
PR #2708 now also caches unchanged static response-format JSON per stage/executor instance. It preserves column-driven formats, row-dependent Responses verbosity, and existing request-helper overrides. Mutable Scala sequences remain uncached to avoid stale requests.
"text" and "json_object" still use setResponseFormat and remain plain selectors. setResponseSchema wraps an inner schema; existing named/full formats keep using setResponseFormat with their name and strictness preserved. Neither path infers missing constraints for raw schemas.
On the current head, 74 Scala tests, 20 Python tests with 116 subtests, and all eight exact notebook cases passed locally against loopback HTTP. A request-encoding benchmark measured 36-79% fewer allocations across 1-, 10-, and 100-property schemas. This is not an LLM/network latency claim, and the schema still accompanies every request.
The review reports now follow master's layout under reviews/pr-2708/. This review-only update is commit f10ee0aa328d7c5aeab300e449b0c80010450391; every non-review file is unchanged from the tested cache implementation.
Full current-head CI passed: Azure build 236151515 completed all 65 jobs successfully. The affected Scala suites passed 74/74 tests with no skips, and OpenAI.ipynb passed all eight live inference cases on Databricks, covering Chat Completions, Responses, and both Prompt API modes with default and custom non-strict settings. Current-head automated review is clear; only required human approval remains.
The helper remains unreleased while the PR is open; the existing setResponseFormat examples above remain available in released builds. This validation does not claim a Fabric AI Functions runtime test.
Activity
github-actions commentedon Sep 6, 2026
Hey Shresth Kumar Gupta (@Srazyy) 👋!
Thank you so much for reporting the issue/feature request 🚨.
Someone from SynapseML Team will be looking to triage this issue soon.
We appreciate your patience.
ranadeepsingh commentedon Sep 15, 2026
Thank you Shresth Kumar Gupta (@Srazyy) for contributing to improving SynapseML and for explaining this use case!
The core structured-output support already exists in SynapseML 1.1.0 and later. It was added in #2428 and refined in #2436.
OpenAIChatCompletion.setResponseFormataccepts a Python dictionary or ScalaMap[String, Any], includingtype: "json_schema"andstrict: true.There are two separate steps:
setResponseFormatto request schema-constrained JSON from the model.from_jsonto turn the returned JSON text into a typed struct. This does not require a Python UDF or per-rowjson.loads.Python
This example assumes an existing Spark session named
sparkand matching SynapseML Python/JVM packages. ConfigureAZURE_OPENAI_API_KEY,AZURE_OPENAI_SERVICE_NAME, andAZURE_OPENAI_DEPLOYMENT_NAMEin your environment. The service name is the Azure OpenAI resource name, not its full URL. Choose a deployment whose model supports structured outputs.Scala
The equivalent Scala example uses the same environment variables and an existing
sparksession:What remains different from the proposed API
OpenAIChatCompletiontakessetMessagesCol, notsetPromptCol, and keeps the response envelope. The JSON text is inchoices[0].message.content.setResponseSchemamethod or direct conversion from a SparkStructTypeto an OpenAI JSON Schema. TheStructTypeabove describes the Spark parsing step, not the model request.setResponseFormatmust be a dictionary/Scala map, not a JSON-encoded string. If your schema starts as a JSON string, parse it once on the driver before configuring the transformer.OpenAIPromptalso supportssetResponseFormat,setPromptTemplate,setPostProcessing("json"), andsetPostProcessingOptions({"jsonSchema": "sentiment STRING, score INT"})in Python.So the JSON Schema request support and UDF-free typed extraction are already available. Direct
StructTypeconversion or asetResponseSchemaconvenience method would be separate enhancements.I checked the released source and validated the published SynapseML 1.1.3 Python package and JVM jars on Spark 3.5.0. Thirteen focused tests passed with no skips, including four mock HTTP requests checking the exact strict-schema payload, typed extraction,
OpenAIPromptJSON post-processing, rejected input forms, and save/load. A separately compiled Scala check also passed. These were local mock-service checks, not live-model inference.Strict schema support does not eliminate service errors, refusals, or truncated responses, and it does not guarantee that extracted values are factually correct. Check
error,finish_reason, and the returned content before consuming a result.If your runtime includes an older SynapseML version, make sure both the Python package and the JVM jars are updated together.
References: original feature, follow-up, released implementation.
fix: preserve Spark port replay and exercise schema-only options
ranadeepsingh commentedon Sep 15, 2026
Thank you again, Shresth Kumar Gupta (@Srazyy), for contributing to improving SynapseML.
The existing
setResponseFormatAPI supports structured outputs, as described above. We can make the experience simpler by accepting the inner JSON Schema directly and adding the API-specific response-format envelope automatically. I've opened PR #2708 for this improvement: #2708.The proposed
setResponseSchemahelper works with OpenAIPrompt, OpenAIChatCompletion, and OpenAIResponses, in both Python and Scala. Its defaults arename="response_schema"andstrict=True.These examples require a build containing PR #2708. The new setter is not in SynapseML 1.1.3. Existing released versions can continue using the full-envelope
setResponseFormatexamples in the earlier reply.By "partial schema," we mean the inner JSON Schema without the outer response-format metadata. The helper preserves that schema; it does not infer
required, addadditionalProperties, convert a SparkStructType, or fill in missing schema definitions. For strict output, supply a schema supported by the selected model.Python
Run in a configured Spark session. Set
AZURE_OPENAI_ENDPOINTto the resource root, such ashttps://<resource>.openai.azure.com, and supplyAZURE_OPENAI_API_KEYandAZURE_OPENAI_DEPLOYMENTthrough your environment or secret-management system. The deployment must support structured outputs and the selected API.Shared setup:
OpenAIPrompt
OpenAIPromptcan parse the JSON into a Spark struct through its existing native JSON post-processing:Use
.setApiType("responses")to send the same prompt through the Responses API instead. The schema setter and post-processing stay the same.Chat Completions
Chat Completions uses
setMessagesCol. Its output remains the response envelope; use Spark'sfrom_jsonto parse the assistant's JSON text:Responses
OpenAIResponsesalso usessetMessagesCol. The JSON text is in anoutput_textcontent item. This expression finds that item without assuming that the first output entry contains text:To customize the metadata on any of these stages, use
.setResponseSchema(schema, name="review_schema", strict=False). Omitting those arguments requests the default name and strict output.Scala equivalents
Scala
The same three APIs accept a
Map[String, Any]. UseListMapwhere JSON property order matters.Shared setup, using the same environment variables:
OpenAIPrompt
As in Python,
.setApiType("responses")selects the Responses API.Chat Completions
Responses
Scala also supports
.setResponseSchema(schema, strict = false)and.setResponseSchema(schema, name = "review_schema", strict = false).These examples use Spark-native parsing rather than Python UDFs. Applications should inspect the error column and handle null parsed results, refusals, and incomplete responses; strict output is not a guarantee that every request produces a usable result.
The exact Python and Scala examples above passed local Spark execution against the PR's generated wrappers and JVM classes, with loopback HTTP checks for request formats and typed results. The updated notebook adds eight live-service cases covering defaults and custom non-strict options across the four routes, including both OpenAIPrompt modes.
Live validation passed on the final head: the updated
OpenAI.ipynbpassed Databricks CI on commit10bead3b18, executing all eight new-option cases. This is actual Azure inference, not just configuration or loopback validation. Final CI checks are green, with all 65 jobs passing. This does not claim Fabric runtime validation; the fork PR's pipeline excludes Fabric E2E.Static-schema caching and format compatibility
PR #2708 now also caches unchanged static response-format JSON per stage/executor instance. It preserves column-driven formats, row-dependent Responses verbosity, and existing request-helper overrides. Mutable Scala sequences remain uncached to avoid stale requests.
"text"and"json_object"still usesetResponseFormatand remain plain selectors.setResponseSchemawraps an inner schema; existing named/full formats keep usingsetResponseFormatwith their name and strictness preserved. Neither path infers missing constraints for raw schemas.On the current head, 74 Scala tests, 20 Python tests with 116 subtests, and all eight exact notebook cases passed locally against loopback HTTP. A request-encoding benchmark measured 36-79% fewer allocations across 1-, 10-, and 100-property schemas. This is not an LLM/network latency claim, and the schema still accompanies every request.
The review reports now follow master's layout under
reviews/pr-2708/. This review-only update is commitf10ee0aa328d7c5aeab300e449b0c80010450391; every non-review file is unchanged from the tested cache implementation.Full current-head CI passed: Azure build 236151515 completed all 65 jobs successfully. The affected Scala suites passed 74/74 tests with no skips, and
OpenAI.ipynbpassed all eight live inference cases on Databricks, covering Chat Completions, Responses, and both Prompt API modes with default and custom non-strict settings. Current-head automated review is clear; only required human approval remains.The helper remains unreleased while the PR is open; the existing
setResponseFormatexamples above remain available in released builds. This validation does not claim a Fabric AI Functions runtime test.style: format schema-only notebook assertions for CI
feat: add schema-only OpenAI structured output configuration
fix: preserve Spark port replay and exercise schema-only options
style: format schema-only notebook assertions for CI
perf: cache static OpenAI response-format JSON
ranadeepsingh commentedon Sep 17, 2026
completed PR