Skip to main content
additionalProperties controls whether fields not listed in properties are allowed. It can be set to either true or false. Setting it to false locks the schema to exactly the declared fields: the model cannot invent extra keys, and your application knows precisely what to expect. This is one of the highest-leverage settings in production schemas. Without it, the model might add a helpful-looking "notes" field that no consumer knows how to handle, or a "timestamp" that conflicts with your own timestamping logic. These extra fields pass validation silently and cause bugs downstream. The nuance is that strict everywhere isn’t always right. Sometimes you need a flexible container, such as a metadata object for vendor-specific keys or an extras bag for forward compatibility. The pattern is: strict at the top level, flexible in one designated location.

Use case

Audit logs that need a strict top-level structure for indexing and compliance, but a metadata object that can carry vendor-specific keys without schema changes.

Schema pattern

{
  "type": "object",
  "properties": {
    "event_id": { "type": "string" },
    "event_type": { "type": "string", "enum": ["login", "logout", "password_reset"] },
    "actor": {
      "type": "object",
      "properties": {
        "user_id": { "type": "string" },
        "ip": { "type": "string" }
      },
      "required": ["user_id", "ip"],
      "additionalProperties": false
    },
    "metadata": {
      "type": "object",
      "additionalProperties": {
        "type": ["string", "number", "boolean", "null"]
      }
    }
  },
  "required": ["event_id", "event_type", "actor", "metadata"],
  "additionalProperties": false
}

Example output

{
  "event_id": "evt_6f1a",
  "event_type": "login",
  "actor": {
    "user_id": "usr_331",
    "ip": "203.0.113.10"
  },
  "metadata": {
    "device": "ios",
    "mfa": true,
    "attempt": 1
  }
}

Why this works

The top-level object and the actor sub-object both have additionalProperties: false, so no unexpected fields can appear in the core structure. Your indexing pipeline knows exactly which fields exist and can map them directly to database columns or search indices. The metadata object uses additionalProperties: { "type": ["string", "number", "boolean", "null"] }; it allows arbitrary keys, but constrains their values to primitive types. This prevents deeply nested or complex structures from sneaking into what should be a flat key-value bag. New metadata keys appear without schema changes, but they can’t break your storage layer.