Validating JSON Requests Using C# and OpenAPI/Swagger

Andy BlackledgeAndy Blackledge
7 min read

Recently, I needed to integrate an internal system that generates JSON with a third-party API. Usefully, I had an OpenAPI specification for the API in question. OpenAPI is a specification for machine-readable interface files for describing, producing, consuming, and visualising RESTful web services. It is also well-known as its previous name of Swagger. You can see and interact with an example specification via the online Swagger Editor.

The specification includes definitions of the objects used by the API. In the Petstore example, the method to add a pet has a payload that takes a Pet object as a its payload.

Pet:
  required:
    - name
    - photoUrls
  type: object
  properties:
    id:
      type: integer
      format: int64
    name:
      type: string
    category:
      $ref: "#/components/schemas/Category"
    photoUrls:
      type: array
      items:
        type: string

What I wanted to do, was use this information to validate the JSON being generated by the internal system before it was sent to the external API. The question was how?

The road to the Microsoft.OpenApi library

My first thought was to manually parse the OpenAPI specification. So I asked Claude.ai:

Please generate a C# method that takes a Swagger definition in JSON and an operation id and returns a JSON schema for the operation request body.

Claude obliged, but the resulting code was far from promising.

using JsonDocument doc = JsonDocument.Parse(swaggerJson);
  JsonElement root = doc.RootElement;

  // Iterate through paths
  foreach (JsonProperty pathProp in root.GetProperty("paths").EnumerateObject())
  {
      foreach (JsonProperty methodProp in pathProp.Value.EnumerateObject())
      {
          if (methodProp.Value.TryGetProperty("operationId", out JsonElement opIdElement)
              && opIdElement.GetString() == operationId)
          {
              // Found the operation, now extract the request body schema
              if (methodProp.Value.TryGetProperty("requestBody", out JsonElement requestBody)
                  && requestBody.TryGetProperty("content", out JsonElement content))
              {
                  foreach (JsonProperty contentType in content.EnumerateObject())
                  {
                      if (contentType.Value.TryGetProperty("schema", out JsonElement schema))
                      {

It occurred to me that my prompt was not high-level enough. I was assuming a certain solution. So instead, I tried the following request.

I am using .NET Framework and C#. I have a Swagger definition for an API. How can I extract the JSON Schema for the request and response body for each operation in the definition?

And the response led me to the Microsoft.OpenApi library.

To extract the JSON Schema for request and response bodies from a Swagger definition in a .NET Framework and C# environment, you can use the Microsoft.OpenApi library. This library provides tools to parse and manipulate OpenAPI (formerly known as Swagger) documents.

As I could see from the generated code, this looked far more like it.

var openApiDocument =
    new OpenApiStreamReader().Read(
        File.OpenRead(swaggerFilePath), out var diagnostic);

foreach (var path in openApiDocument.Paths)
{
    foreach (var operation in path.Value.Operations)
    {
        var operationType = operation.Key.ToString();
        var operationId = operation.Value.OperationId;

        // Extract request body schema
        if (operation.Value.RequestBody != null &&
            operation.Value.RequestBody.Content.TryGetValue(
                "application/json", out var requestMediaType))
        {
            var requestSchema = requestMediaType.Schema;

Getting Claude.ai's code to work

This was great, but it only had one flaw. It didn't work. When I tried running the code I got the following exception.

Newtonsoft.Json.JsonSerializationException
  HResult=0x80131500
  Message=Self referencing loop detected for property 'HostDocument' with type 'Microsoft.OpenApi.Models.OpenApiDocument'. Path 'Properties.category.Reference.HostDocument.Paths['/pet/{petId}/uploadImage'].Operations.Post.Tags[0].Reference'.
  Source=Newtonsoft.Json

The exceptioin occurred on the following line.

var schemaData = JsonConvert.SerializeObject(openApiSchema);

Claude.ai had predicted that something called OpenApiSchema was a serializable JSON schema. It turns out that it isn't, so I had to go back to some old-fashioned searching on StackOverflow. My search turned up the following question, Get the JSON Schema's from a large OpenAPI Document OR using NewtonSoft and resolve refs. The accepted answer had the following code (slightly abbreviated for this post).

var reader = new OpenApiStreamReader();
var result =
    await reader.ReadAsync(new FileStream(file.FullName, FileMode.Open));

foreach (var schemaEntry in result.OpenApiDocument.Components.Schemas)
{
    var schemaFileName = schemaEntry.Key + ".json";
    var outputPath =
        Path.Combine(outputDir, schemaFileName + "-Schema.json");

    using var fileStream = new FileStream(outputPath, FileMode.CreateNew);
    using var writer = new StreamWriter(fileStream);

    var writerSettings =
        new OpenApiWriterSettings()
        {
            InlineLocalReferences = true,
            InlineExternalReferences = true
        };

    schemaEntry.Value
        .SerializeAsV2WithoutReference(
            new OpenApiJsonWriter(writer, writerSettings));
}

Sure enough, when I ran this code against the Petstore OpenAPI document, it successfully created the following schema files:

List of exported JSON schemas

Opening up User.json-Schema.json, I could see that the contents looked to my eye like a valid JSON schema.

{
  "type": "object",
  "properties": {
    "id": {
      "format": "int64",
      "type": "integer"
    },
    "petId": {
      "format": "int64",
      "type": "integer"
    },
    "quantity": {
      "format": "int32",
      "type": "integer"
    },
    "shipDate": {
      "format": "date-time",
      "type": "string"
    },
    "status": {
      "description": "Order Status",
      "enum": ["placed", "approved", "delivered"],
      "type": "string"
    },
    "complete": {
      "type": "boolean"
    }
  },
  "xml": {
    "name": "Order"
  }
}

The key here was the use of the SerializeAsV2WithoutReference method with the OpenApiJsonWriter and OpenApiWriterSettings. Together they control how an OpenApiSchema instance is serialized. In this case, we want to inline all references to get a self-contained schema.

var writerSettings =
    new OpenApiWriterSettings()
    {
        InlineLocalReferences = true,
        InlineExternalReferences = true
    };

schemaEntry.Value
    .SerializeAsV2WithoutReference(
        new OpenApiJsonWriter(writer, writerSettings));

Now I had a way of extracting the schemas, I could move on to my ultimate aim of using the schemas to validate my dynamically-created requests.

Creating my OpenApiSchemaValidator

As I mentioned in my Designing a CDK State Machine Builder post, I like to build software using an API-first approach. That is, before jumping into the component implementation, I imagine it exists and write the code the calling code. This allows me to quickly iterate over the external API until it feels it has the right level of expression and abstraction.

With this in mind, I imagined a OpenApiSchemaValidator class and then iterated on the calling code until I got the following.

var validator =
    new OpenApiSchemaValidator(
        new FileStream("petstore.swagger.json", FileMode.Open));

var validationResult =
    validator.ValidateRequestBodyJson(
        operationId: "addPet", bodyJson: petJson);

if (validationResult.IsValid)
    Console.WriteLine("Request JSON is valid");
else
    Console.WriteLine(
        $"Request JSON had the following errors: \n- " +
        $"{string.Join("\n- ", validationResult.Errors)}");

I decided to split the creation of OpenApiSchemaValidator from the method call. This would allow the creation of the instance to process the OpenAPI document and avoid this static overhead on each validation call.

However, when I dived int the implementation, I had to make a small change. I was using the NJsonSchema for .NET NuGet package to validate the JSON requests against the JSON schemas. It turned out that the method to parse the JSON schema was async. Since I could not have this in a constructor, I had to have an async factory method instead.

var validator =
    await OpenApiSchemaValidator.CreateAsync(
        new FileStream("petstore.swagger.json", FileMode.Open));

The implementation is shown below.

private readonly IReadOnlyDictionary<string, JsonSchema> _requestBodyJsonSchemas;

private OpenApiSchemaValidator(IDictionary<string, JsonSchema> jsonSchemas) =>
    _requestBodyJsonSchemas = new Dictionary<string, JsonSchema>(jsonSchemas);

public static async Task<OpenApiSchemaValidator> CreateAsync(Stream openApiStream)
{
    // Get the operations with JSON request bodies

    var openApiDocument =
        new OpenApiStreamReader().Read(openApiStream, out _);

    var requestBodyOperations =
        openApiDocument.Paths
            .SelectMany(p => p.Value.Operations)
            .Where(o =>
                o.Value.RequestBody != null &&
                o.Value.RequestBody.Content.ContainsKey("application/json"));

    // Convert the OpenAPI schemas to JSON schemas and index by Operation Id

    var jsonSchemas = new Dictionary<string, JsonSchema>();

    foreach (var operation in requestBodyOperations)
    {
        var openApiSchema =
            operation.Value.RequestBody.Content["application/json"].Schema;
        var jsonSchema =
            await JsonSchema.FromJsonAsync(SerializeToJsonSchema(openApiSchema));
        jsonSchemas.Add(operation.Value.OperationId, jsonSchema);
    }

    return new(jsonSchemas);
}

The SerializeToJsonSchema method used the SerializeAsV2WithoutReference method as discussed earlier.

With the dictionary of schemas in place, applying them was straightforward.

public OpenApiSchemaValidationResult ValidateRequestBodyJson(
    string operationId,
    string bodyJson)
{
    if (_requestBodyJsonSchemas.TryGetValue(operationId, out var jsonSchema))
    {
        // Validate the JSON against the schema

        var errors = jsonSchema.Validate(JToken.Parse(bodyJson));

        return
            new OpenApiSchemaValidationResult
            {
                IsValid = errors.Count == 0,
                Errors = errors.Select(e => $"{e.Path}: {e.Kind}")
            };
    }

    throw new ArgumentException(
        $"Operation does not have a JSON request body: {operationId}",
        nameof(operationId));
}

And with that in place, I could test the validation with an empty JSON object.

var validationResult =
    validator.ValidateRequestBodyJson(operationId: "addPet", bodyJson: "{}");

Which resulted in the following output to be written to the console, successfully reporting that the JSON request had two missing properties.

Request JSON had the following errors:
- #/name: PropertyRequired
- #/photoUrls: PropertyRequired

Summary

Thanks to Claude.ai and StackOverflow, I was able to implement the functionality I wanted, in a class only 95 lines long. The key piece of information was the existence of the Microsoft.OpenApi namespace. This did all the heavy lifting of reading the OpenApi document and outputting the request schemas as JSON schemas. From there, it was straightforward to use NJsonSchema to do the validation. The resulting OpenApiSchemaValidator class can be found on GitHub here.

Now that I had a way of reading the OpenAPI document, I realised that I could do more with it than just validating request bodies. An OpenAPI document includes the paths, parameters, and more for each endpoint. I asked myself, would it be possible to build a dynamic OpenAPI client that I could use as follows?

var petStoreClient =
    await OpenApiClient.CreateAsync(
        new FileStream("petstore.swagger.json", FileMode.Open),
        "https://petstore.swagger.io/v2");

var getPetByIdResponse =
    await petStoreClient.PerformAsync("getPetById", [("petId", "0")]);

One for another post I think.

0
Subscribe to my newsletter

Read articles from Andy Blackledge directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Andy Blackledge
Andy Blackledge