OpenAI API: Structured Outputs – generate JSON schema from a class in C#

What problem does the Structured Outputs feature solve

When using GPT models from the code, we developers don’t normally want the output to be in a natural language like in ChatGPT conversations. Instead, we want it to have some structure, and the lingua franca today for data interchange is JSON format.

Until recently, reliably achieving JSON outputs in the same format was difficult in GPT models. We had several options trying:

  1. We could be very specific about the expected output format in the prompt, but this often failed. I provided several examples in my other blog post.
  2. We could use JSON mode to ensure the response is a valid JSON. I described JSON mode and some of its pitfalls earlier.
  3. Now, we have a feature called Structured Outputs that allows us to define a strict JSON Schema for the response.

Minimal example using REST API

Here’s an example you can run using Postman, Insomnia or similar HTTP clients:

// POST https://api.openai.com/v1/chat/completions

{
  "model": "gpt-4o-2024-08-06",
  "messages": [
    {
      "role": "user",
      "content": "Provide an example of a english noun at C1 language learning level."
    }
  ],
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "word_response",
      "strict": true,
      "schema": {
        "type": "object",
        "properties": {
          "Word": {
            "type": "string"
          },
          "WordTranslatedToSpanish": {
            "type": "string",
            "description": "The word translated to Spanish, preceded by an article (el/la)."
          }
        },
        "required": ["Word", "WordTranslatedToSpanish"],
        "additionalProperties": false
      }
    }
  }
}

Code language: JSON / JSON with Comments (json)

JSON schema is unfortunately a bit verbose by its nature, but the point of the demo is to show that even though the prompt didn’t mention anything about the output format requirements, we now receive the following response:

{
  "Word": "Hypothesis",
  "WordTranslatedToSpanish": "La hipótesis"
}Code language: JSON / JSON with Comments (json)

I find it super convenient, and I see two benefits here:

  1. Adherence to the schema is guaranteed, which makes applications using the API significantly more reliable without the need to implement workarounds, endlessly fine-tune the prompt, or retry requests if they fail to conform to the schema
  2. The request became more… “promptless” 🙂 You can see how I described the output details in the schema while leaving the prompt very generic. I didn’t ask for the word’s translation in the prompt, yet it was filled correctly based on the schema provided. I think this declarative way of describing output will become a preferred way to use Generative AI APIs from the applications. It plays well with the Object Generative Fill programming pattern I proposed recently.

Generating schema in C# using Newtonsoft.Json.Schema

The fun part of using Structured Outputs from the code is that we can generate the JSON schema from a class. Here’s an example in C#:

// code uses the Newtonsoft.Json.Schema NuGet package to convert class to a JSON schema

// Declare the format of the GPT model's response as a C# class
public class Country
{
    [Description("Country name preferred in English language")]
    public string InternationalName { get; set; }

    public int EstimatedPopulation { get; set; }

    [Description("Names of 3 people from that country most recognizable abroad")]
    public List<string> FamousPeopleExample { get; set; }
}

internal class Program
{
    static async Task Main(string[] args)
    {
        // Prepare the JSON schema
        JSchemaGenerator generator = new JSchemaGenerator();
        generator.DefaultRequired = Required.Always; // required by OpenAI

        JSchema schema = generator.Generate(typeof(Country));
        schema.AllowAdditionalProperties = false; // required by OpenAI

        var schemaJson = schema.ToString();

        Console.WriteLine(schemaJson);
    }
}
Code language: C# (cs)

The above outputs schema that can be used in the API call:

{
   "type": "object",
   "additionalProperties": false,
   "properties": {
      "InternationalName": {
         "description": "Country name preferred in English language",
         "type": "string"
      },
      "EstimatedPopulation": {
         "type": "integer"
      },
      "FamousPeopleExample": {
         "description": "Names of 3 people from that country most recognizable abroad",
         "type": "array",
         "items": {
            "type": "string"
         }
      }
   },
   "required": [
      "InternationalName",
      "EstimatedPopulation",
      "FamousPeopleExample"
   ]
}Code language: JSON / JSON with Comments (json)

And the response matched the expectation when posted with the simple prompt “Provide information about Poland.“:

{
  "InternationalName": "Poland",
  "EstimatedPopulation": 38386159,
  "FamousPeopleExample": [
    "Marie Curie",
    "Frédéric Chopin",
    "Nicolaus Copernicus"
  ]
}Code language: JSON / JSON with Comments (json)

A warning regarding Newtonsoft.Json.Schema library

One thing I haven’t realized is that Newtonsoft.Json.Schema limits schema generation to 10 per hour and requires a license to exceed this limit! So please consider the license limitation before committing to this library in your project.

I was surprised with the following error: Newtonsoft.Json.Schema.JSchemaException: ‘The free-quota limit of 10 schema generations per hour has been reached. Please visit http://www.newtonsoft.com/jsonschema to upgrade to a commercial license.’

The status of feature support in popular libraries

The Structured Outputs support is now available in popular libraries like Betalgo.OpenAI and the official one, OpenAI.

Here’s a usage example for Betalgo. Unfortunately, I don’t like the design. They rushed to implement the feature, re-inventing the wheel and independently modeling the JSON Schema type. This prevents library users from generating JSON Schema from a class using other libraries and makes it much less attractive.

The OpenAI library did a much better job (see docs and usage example for Structured Output here). The library’s user needs to provide schema as BinaryData (and we can easily convert it from a string), so we can use the schema generated in the earlier section of this article.

You can also find a minimal C# example of generating JSON Schema from a C# class model and using it to query OpenAI API in my GitHub repository. The source code is here.
No comments yet, you can leave the first one!

Leave a Comment