mykeels.com

Accepting dynamic body properties in endpoints with OpenAPI schema

I faced a bug that taught me this very hard lesson that I must share

Accepting dynamic body properties in endpoints with OpenAPI schema

I faced a bug that taught me this very hard lesson that I must share, but we'll get to that later. First, let's understand the problem.

The problem

I have two services, ServiceA and ServiceB.

ServiceA uses a dynamic type language like Python or JavaScript to process data. It has an endpoint that accepts dynamic body properties meaning it can accept any property name and value. It could be:

{
    "name": "John",
    "metadata": {
        "foo": "bar"
    }
}

today, and tomorrow, it could be:

{
    "name": "John",
    "metadata": {
        "bar": "baz" // properties could be anything
    },
    "address": "123 Main St, Anytown, USA", // property may not be defined in the schema
    "phone": "123-456-7890" // property may not be defined in the schema
}

ServiceB uses a static type language like C# or Java to process data. It has an endpoint that accepts a specific schema. The endpoint then performs some operations like validation, authorization, etc, and sends its payload to ServiceA.

The bug

ServiceA emits an OpenAPI schema that describes the endpoint's known properties. ServiceB uses a static typed client, generated from ServiceA's OpenAPI schema, to send its payload to ServiceA.

This means that ServiceB can only send payloads that match the known properties in the schema described by ServiceA's OpenAPI schema. Any additional properties will be ignored. This is a huge problem because ServiceA will receive the incomplete payload and act on it as if it were complete.

How do we prevent this?

The prevention

We can prevent this by using the additionalProperties property in the OpenAPI schema. This property is used to indicate whether the endpoint accepts additional properties.

{
    ...,
    "additionalProperties": true
}

e.g. to describe the endpoint in ServiceA's OpenAPI schema, we would do:

{
    "properties": {
        "name": {
            "type": "string" // known property
        },
        "metadata": {
            "type": "object",
            "additionalProperties": true // note: this is the important part
        }
    },
    "additionalProperties": true // note: this is the important part
}

We are saying that the properties in the body and metadata objects can be anything. This informs the client generators to allow additional properties in the body and metadata objects.

A detour into handling additional properties in C#

As an detour, In C#, I've found that some client generators like openapi-generator-cli will ensure that the body type is Dictionary<string, object> so that it can accept additional properties.

Send(Dictionary<string, object> body);

This can be a problem because it means the known properties are now missing from the body type. How do we fix this?

public class Body: Dictionary<string, object> // inherit from Dictionary<string, object> to allow additional properties
{
    public string name
    {
        get => (string)this["name"] ?? throw new ArgumentException("name is required");
        set => this["name"] = value;
    }

    public Dictionary<string, object> metadata
    {
        get
        {
            var obj = (JObject)this["metadata"];
            return obj?.ToObject<Dictionary<string, object>>() ?? throw new ArgumentException("metadata is required");
        }
        set => this["metadata"] = JObject.FromObject(value);
    }
}
var body = new Body(); // can be received from the endpoint
Send(body); // even though Send expects a Dictionary<string, object>, the Body supertype can be passed in

This way, we can have a body type that can accept additional properties while still having the known properties.

Notice how serialization and deserialization of the Body type works in this example below:

Key takeaways

  • Always use additionalProperties: true in your OpenAPI schemas when your endpoints accept dynamic properties
  • Test your client generation to ensure additional properties are properly handled
  • Consider the trade-offs between static typing and dynamic flexibility when designing your APIs
  • Document your OpenAPI schema clearly so other developers understand what properties are required vs. optional

The bug I mentioned at the beginning?

It happened because ServiceA was silently dropping additional properties that ServiceB was sending, leading to incomplete data processing. This cost us hours of debugging that could have been prevented with proper OpenAPI schema configuration or documentation of expectations.

Remember: when working with services, the contract between them (I highly recommend using OpenAPI schema) is crucial. Make sure it accurately represents what your endpoints can actually handle, not just what you think they should handle.

Visualization: Accepting dynamic body properties in endpoints with OpenAPI schema

Related Articles

Tags