{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://vlatte.dev/schemas/http.schema.json",
  "title": "Mock Definition",
  "description": "Describes an HTTP mock definition. The decoder in mockhttp/mock_schema.go is the reference; tests keep this schema in step with it.",
  "type": "object",
  "properties": {
    "$schema": { "description": "The schema an editor checks the file with, such as https://vlatte.dev/schemas/http.schema.json. The server ignores it.", "type": "string" },
    "id": {
      "description": "ID of the mock, in place of a generated one.",
      "type": "string",
      "pattern": "^[A-Za-z0-9._~-]+$"
    },
    "name": { "type": "string" },
    "priority": { "description": "Mocks with a lower priority are matched first.", "type": "integer" },
    "enabled": { "$ref": "#/$defs/bool" },
    "delay": { "$ref": "#/$defs/duration" },
    "repeat": { "type": "integer", "minimum": 1 },
    "scenario": {
      "type": "object",
      "properties": {
        "name": { "type": "string" },
        "required_state": { "type": "string" },
        "new_state": { "type": "string" },
        "key": { "description": "Template of the key of the scenario's instance a request plays in, such as {{ .Request.Header.Get \"X-Test-Id\" }}: each key moves an instance of its own. Empty plays in the shared instance.", "type": "string" }
      },
      "required": [ "name" ],
      "additionalProperties": false
    },
    "on_complete": { "type": "array", "items": { "$ref": "#/$defs/on_complete_event" } },
    "pipes": { "description": "Faults the body of each answer passes through, in order.", "type": "array", "items": { "$ref": "#/$defs/fault" } },
    "wire_pipes": { "description": "Faults every byte of each answer passes through, its head included, after pipes. Needs HTTP/1.x.", "type": "array", "items": { "$ref": "#/$defs/fault" } },
    "upstream_pipes": { "description": "Faults the body a proxy reply sends on passes through.", "type": "array", "items": { "$ref": "#/$defs/fault" } },
    "request": {
      "type": "object",
      "properties": {
        "scheme": { "$ref": "#/$defs/matcher" },
        "method": { "$ref": "#/$defs/matcher" },
        "host": { "description": "Host the request names, without its port: text, compared ignoring case, or a matcher list.", "$ref": "#/$defs/matcher" },
        "client_ip": { "description": "Address of the client, without its port.", "$ref": "#/$defs/matcher" },
        "path": { "$ref": "#/$defs/matcher" },
        "path_template": { "description": "Path with parameters, such as /users/{id}: a parameter is a whole segment, and takes any segment but an empty one.", "type": "string", "pattern": "^/" },
        "path_params": { "description": "Parameters of path_template, by name.", "$ref": "#/$defs/keyed_matcher" },
        "url": { "description": "URL: an absolute one, compared ignoring case; a relative one, such as /items?page=2, whose path and query must be the same, the parameters in any order, each with its values in order; or a matcher list.", "$ref": "#/$defs/matcher" },
        "url_match": { "type": "string", "format": "regex" },
        "fragment": { "description": "URL fragment: the text after a \"#\" of the request target. Only clients that write a \"#\" into the request target send one.", "$ref": "#/$defs/matcher" },
        "query": { "$ref": "#/$defs/keyed_matcher" },
        "queries": { "description": "Query parameters with many values, by name: every value, as a list.", "$ref": "#/$defs/keyed_list_matcher" },
        "header": { "$ref": "#/$defs/keyed_matcher" },
        "headers": { "description": "Headers with many values, by name: every value, as a list.", "$ref": "#/$defs/keyed_list_matcher" },
        "cookie": { "description": "Cookies, by name, which keeps its case.", "$ref": "#/$defs/keyed_matcher" },
        "basic_auth": {
          "description": "Credentials of a Basic Authorization header.",
          "type": "object",
          "properties": {
            "username": { "$ref": "#/$defs/matcher" },
            "password": { "$ref": "#/$defs/matcher" }
          },
          "required": [ "username", "password" ],
          "additionalProperties": false
        },
        "form": { "description": "Fields of a urlencoded or multipart form body, by name.", "$ref": "#/$defs/keyed_matcher" },
        "forms": { "description": "Fields of a form body with many values, by name: every value, as a list.", "$ref": "#/$defs/keyed_list_matcher" },
        "form_file": {
          "description": "Files of a multipart form body, by field name: a pattern, or a list of patterns, each met by some file of the field.",
          "type": "object",
          "patternProperties": {
            "^.*$": {
              "oneOf": [
                { "$ref": "#/$defs/file_pattern" },
                { "type": "array", "items": { "$ref": "#/$defs/file_pattern" } }
              ]
            }
          }
        },
        "body": { "$ref": "#/$defs/matcher" },
        "custom": {
          "description": "Matchers of the whole request, such as custom ones of the server: a list of matcher lists, or names.",
          "type": "array",
          "items": { "oneOf": [ { "type": "string" }, { "type": "array", "minItems": 1 } ] }
        }
      },
      "minProperties": 1,
      "additionalProperties": false,
      "dependentRequired": { "path_params": [ "path_template" ] },
      "allOf": [
        { "not": { "required": [ "url", "url_match" ] } },
        { "not": { "required": [ "url", "path" ] } },
        { "not": { "required": [ "url_match", "path" ] } }
      ]
    },
    "response": { "$ref": "#/$defs/reply" },
    "response_sequence": {
      "type": "object",
      "properties": {
        "responses": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/reply" } },
        "after_ended": { "$ref": "#/$defs/reply" }
      },
      "required": [ "responses" ],
      "additionalProperties": false
    },
    "response_random": {
      "type": "object",
      "properties": {
        "responses": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/reply" } },
        "seed": { "type": "integer" }
      },
      "required": [ "responses" ],
      "additionalProperties": false
    },
    "proxy": {
      "type": "object",
      "properties": {
        "target": { "type": "string", "pattern": "^[Hh][Tt][Tt][Pp][Ss]?://[^/]+" },
        "request_header": { "$ref": "#/$defs/header_map" },
        "header": { "$ref": "#/$defs/header_map" },
        "remove_request_headers": { "type": "array", "items": { "type": "string" } },
        "trim_prefix": { "type": "string" },
        "trim_suffix": { "type": "string" },
        "timeout": { "$ref": "#/$defs/duration" },
        "ssl_verify": { "$ref": "#/$defs/bool" },
        "x_forwarded": { "$ref": "#/$defs/bool" },
        "preserve_host": { "$ref": "#/$defs/bool" }
      },
      "required": [ "target" ],
      "additionalProperties": false
    },
    "proxy_all": {
      "description": "Forward what no other mock matches to a target, as proxy does: any request but a CONNECT, and the ones in absolute form a forward proxy sends on to their hosts. It comes last, at the highest priority, unless priority says otherwise, and needs no request.",
      "$ref": "#/properties/proxy"
    },
    "ext": { "description": "Settings for the MockFileHandlers of the server.", "type": "object" }
  },
  "anyOf": [ { "required": [ "request" ] }, { "required": [ "proxy_all" ] } ],
  "additionalProperties": false,
  "allOf": [
    { "not": { "required": [ "response", "response_sequence" ] } },
    { "not": { "required": [ "response", "response_random" ] } },
    { "not": { "required": [ "response", "proxy" ] } },
    { "not": { "required": [ "response", "proxy_all" ] } },
    { "not": { "required": [ "response_sequence", "response_random" ] } },
    { "not": { "required": [ "response_sequence", "proxy" ] } },
    { "not": { "required": [ "response_sequence", "proxy_all" ] } },
    { "not": { "required": [ "response_random", "proxy" ] } },
    { "not": { "required": [ "response_random", "proxy_all" ] } },
    { "not": { "required": [ "proxy", "proxy_all" ] } }
  ],
  "$defs": {
    "duration": {
      "description": "Whole milliseconds, or an unsigned Go duration such as 1.5s.",
      "oneOf": [
        { "type": "integer", "minimum": 0, "maximum": 9223372036854 },
        { "type": "string", "pattern": "^(0|(([0-9]+(\\.[0-9]*)?|\\.[0-9]+)(ns|us|\u00b5s|\u03bcs|ms|s|m|h))+)$" }
      ]
    },
    "bool": {
      "description": "A bool, or what Go's strconv.ParseBool reads: 1, t, T, TRUE, true, True and their false forms, or the number 1 or 0.",
      "oneOf": [
        { "type": "boolean" },
        { "enum": [ 0, 1, "1", "t", "T", "TRUE", "true", "True", "0", "f", "F", "FALSE", "false", "False" ] }
      ]
    },
    "fault": {
      "description": "A fault: an object whose type names it, and whose other keys are its parameters, such as {\"type\": \"latency\", \"latency\": \"300ms\"}.",
      "type": "object",
      "properties": { "type": { "enum": [ "empty_response", "malformed_response_chunk", "random_data_then_close", "connection_reset_by_peer", "chunked_dribble_delay", "latency", "bandwidth", "slicer", "slow_close", "timeout", "limit_data", "connection_reset_by_peer_after" ] } },
      "required": [ "type" ],
      "oneOf": [
        {
          "description": "Answers with nothing: no head, no body.",
          "properties": { "type": { "const": "empty_response" } },
          "required": [ "type" ]
        },
        {
          "description": "Sends a chunk of the body that HTTP can't read.",
          "properties": { "type": { "const": "malformed_response_chunk" } },
          "required": [ "type" ]
        },
        {
          "description": "Sends random bytes, then closes the connection.",
          "properties": { "type": { "const": "random_data_then_close" } },
          "required": [ "type" ]
        },
        {
          "description": "Resets the connection.",
          "properties": { "type": { "const": "connection_reset_by_peer" } },
          "required": [ "type" ]
        },
        {
          "description": "Sends the body in parts, spread over a time.",
          "properties": { "type": { "const": "chunked_dribble_delay" }, "chunks": { "description": "How many parts the body is sent in.", "type": "integer", "minimum": 1 }, "total": { "description": "When the body ends.", "$ref": "#/$defs/duration" } },
          "required": [ "type", "chunks", "total" ]
        },
        {
          "description": "Delays each write.",
          "properties": { "type": { "const": "latency" }, "latency": { "description": "How late each write arrives.", "$ref": "#/$defs/duration" }, "jitter": { "description": "How much the latency varies by, up to the latency. 0 by default.", "$ref": "#/$defs/duration" } },
          "required": [ "type", "latency" ]
        },
        {
          "description": "Limits the bytes sent each second.",
          "properties": { "type": { "const": "bandwidth" }, "rate": { "description": "Bytes per second, or a common link.", "oneOf": [ { "type": "integer", "minimum": 1 }, { "enum": [ "gprs", "dial_up", "2g", "good_2g", "3g", "good_3g", "dsl", "4g", "wifi", "fiber" ] } ] } },
          "required": [ "type", "rate" ]
        },
        {
          "description": "Sends the body in slices.",
          "properties": { "type": { "const": "slicer" }, "average_size": { "description": "How many bytes a slice has, on average.", "type": "integer", "minimum": 1 }, "size_variation": { "description": "How many bytes a slice varies by, below average_size. 0 by default.", "type": "integer", "minimum": 0 }, "delay": { "description": "How long to wait before each slice. 0 by default.", "$ref": "#/$defs/duration" } },
          "required": [ "type", "average_size" ]
        },
        {
          "description": "Delays the end of the answer.",
          "properties": { "type": { "const": "slow_close" }, "delay": { "description": "How long the end of the answer waits.", "$ref": "#/$defs/duration" } },
          "required": [ "type", "delay" ]
        },
        {
          "description": "Sends nothing, then ends the answer.",
          "properties": { "type": { "const": "timeout" }, "after": { "description": "When the answer ends. 0, the default, never ends it.", "$ref": "#/$defs/duration" } },
          "required": [ "type" ]
        },
        {
          "description": "Closes the connection after some bytes.",
          "properties": { "type": { "const": "limit_data" }, "bytes": { "description": "How many bytes pass before the connection closes.", "type": "integer", "minimum": 0 } },
          "required": [ "type", "bytes" ]
        },
        {
          "description": "Resets the connection after a time.",
          "properties": { "type": { "const": "connection_reset_by_peer_after" }, "after": { "description": "When the connection is reset.", "$ref": "#/$defs/duration" } },
          "required": [ "type", "after" ]
        }
      ],
      "unevaluatedProperties": false
    },
    "matcher": {
      "description": "A string, compared as is, or a matcher list such as [\"contains\", \"x\"].",
      "oneOf": [
        { "type": "string" },
        { "type": "array", "minItems": 1 }
      ]
    },
    "keyed_matcher": {
      "type": "object",
      "patternProperties": {
        "^.*$": { "$ref": "#/$defs/matcher" }
      }
    },
    "keyed_list_matcher": {
      "description": "Matcher lists by name, such as [\"contains\", \"x\"]: each gets every value, a list, which text never equals.",
      "type": "object",
      "patternProperties": {
        "^.*$": { "type": "array", "minItems": 1 }
      }
    },
    "file_pattern": {
      "description": "What a file of a multipart form must have. {} matches any file.",
      "type": "object",
      "properties": {
        "filename": { "description": "The file's name, as Go reads it: its base.", "$ref": "#/$defs/matcher" },
        "content_type": { "description": "The media type: in lower case, without parameters, or empty when absent.", "$ref": "#/$defs/matcher" },
        "header": { "description": "Headers of the file's part.", "$ref": "#/$defs/keyed_matcher" },
        "body": { "description": "The content, as text.", "$ref": "#/$defs/matcher" },
        "body_file": { "description": "A file the content must equal. Relative paths start at the mock file's directory.", "type": "string", "minLength": 1 },
        "every": { "description": "Match when every file of the field matches, rather than some file.", "$ref": "#/$defs/bool" }
      },
      "additionalProperties": false,
      "not": { "required": [ "body", "body_file" ] }
    },
    "reply": {
      "type": "object",
      "properties": {
        "status": { "type": "integer", "minimum": 100, "maximum": 599 },
        "header": { "$ref": "#/$defs/header_map" },
        "header_template": { "description": "Headers whose values are templates.", "$ref": "#/$defs/header_map" },
        "trailer": { "$ref": "#/$defs/header_map" },
        "body": { "description": "Text as is; anything else as JSON.", "not": { "type": "null" } },
        "body_base64": { "description": "The bytes of the body in standard base64, for a body that isn't UTF-8 text, such as an image. Line breaks are ignored.", "type": "string", "contentEncoding": "base64", "pattern": "^[\\r\\n]*(([A-Za-z0-9+/][\\r\\n]*){4})*(([A-Za-z0-9+/][\\r\\n]*){2}=[\\r\\n]*=|([A-Za-z0-9+/][\\r\\n]*){3}=)?[\\r\\n]*$" },
        "body_file": { "description": "Relative paths start at the mock file's directory. The name is a template: one rendered from the request must stay inside that directory.", "type": "string", "minLength": 1 },
        "multipart": { "$ref": "#/$defs/multipart" },
        "event_stream": { "$ref": "#/$defs/event_stream" },
        "template": {
          "type": "object",
          "properties": {
            "enabled": { "description": "Render the body as a template: a text body, the content of body_file, the text of multipart parts, or the text of event_stream events.", "$ref": "#/$defs/bool" },
            "data": { "description": "Data the reply's templates read as .Extra.", "oneOf": [ { "type": "object" }, { "type": "array" } ] }
          },
          "additionalProperties": false
        },
        "encoding": { "enum": [ "gzip" ] }
      },
      "additionalProperties": false,
      "allOf": [
        { "not": { "required": [ "body", "body_file" ] } },
        { "not": { "required": [ "body", "body_base64" ] } },
        { "not": { "required": [ "body", "multipart" ] } },
        { "not": { "required": [ "body_base64", "body_file" ] } },
        { "not": { "required": [ "body_base64", "multipart" ] } },
        { "not": { "required": [ "body_file", "multipart" ] } },
        { "not": { "required": [ "body", "event_stream" ] } },
        { "not": { "required": [ "body_base64", "event_stream" ] } },
        { "not": { "required": [ "body_file", "event_stream" ] } },
        { "not": { "required": [ "multipart", "event_stream" ] } }
      ]
    },
    "multipart": {
      "description": "A multipart body, written with Go's multipart writer. It sets the Content-Type, with its boundary.",
      "type": "object",
      "properties": {
        "type": { "description": "The subtype only, such as form-data, mixed, related or x-mixed-replace.", "type": "string", "pattern": "^[!#$%&'*+.^_`|~{}0-9A-Za-z-]+$" },
        "boundary": { "description": "1 to 70 letters, digits or '()+_,-./:=? characters, not ending in a space. Random when left out.", "type": "string", "pattern": "^[0-9A-Za-z'()+_,./:=? -]{0,69}[0-9A-Za-z'()+_,./:=?-]$" },
        "params": {
          "description": "More parameters of the Content-Type, such as type and start.",
          "type": "object",
          "propertyNames": { "pattern": "^[!#$%&'*+.^_`|~{}0-9A-Za-z-]+$", "not": { "pattern": "^[Bb][Oo][Uu][Nn][Dd][Aa][Rr][Yy]$" } },
          "additionalProperties": { "type": "string" }
        },
        "parts": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/multipart_part" } }
      },
      "required": [ "type", "parts" ],
      "additionalProperties": false,
      "if": { "properties": { "type": { "pattern": "^[Ff][Oo][Rr][Mm]-[Dd][Aa][Tt][Aa]$" } }, "required": [ "type" ] },
      "then": { "properties": { "parts": { "items": { "required": [ "name" ] } } } }
    },
    "multipart_part": {
      "type": "object",
      "properties": {
        "name": { "description": "Name of the part, in its Content-Disposition: required in form-data.", "type": "string" },
        "filename": { "type": "string" },
        "content_type": { "description": "JSON bodies default to application/json, and parts with a filename to application/octet-stream.", "type": "string" },
        "header": { "$ref": "#/$defs/header_map" },
        "body": { "description": "Text as is; anything else as JSON.", "not": { "type": "null" } },
        "body_base64": { "description": "The bytes of the body in standard base64, for a body that isn't UTF-8 text, such as an image. Line breaks are ignored.", "type": "string", "contentEncoding": "base64", "pattern": "^[\\r\\n]*(([A-Za-z0-9+/][\\r\\n]*){4})*(([A-Za-z0-9+/][\\r\\n]*){2}=[\\r\\n]*=|([A-Za-z0-9+/][\\r\\n]*){3}=)?[\\r\\n]*$" },
        "body_file": { "description": "Read at each reply. Relative paths start at the mock file's directory.", "type": "string", "minLength": 1 },
        "delay": { "description": "How long a reply waits before writing the part.", "$ref": "#/$defs/duration" }
      },
      "additionalProperties": false,
      "allOf": [
        { "not": { "required": [ "body", "body_base64" ] } },
        { "not": { "required": [ "body", "body_file" ] } },
        { "not": { "required": [ "body_base64", "body_file" ] } }
      ]
    },
    "event_stream": {
      "description": "A Server-Sent Events body, sent event by event, each flushed as it goes. Its Content-Type is text/event-stream unless header sets one.",
      "type": "object",
      "properties": {
        "events": { "type": "array", "items": { "$ref": "#/$defs/event_stream_event" } },
        "retry": { "description": "Reconnection time sent first, which clients read in whole milliseconds.", "$ref": "#/$defs/duration" },
        "event_delay": { "description": "How long to wait before each event without a delay of its own.", "$ref": "#/$defs/duration" },
        "heartbeat": { "description": "Send a comment each time the stream waits this long without sending anything.", "$ref": "#/$defs/duration" },
        "keep_open": { "description": "Keep the stream open after its last event, until the client leaves.", "$ref": "#/$defs/bool" },
        "resume": { "description": "Send a request with a Last-Event-ID only the events after the one with that id.", "$ref": "#/$defs/bool" }
      },
      "additionalProperties": false
    },
    "event_stream_event": {
      "type": "object",
      "properties": {
        "event": { "description": "Event type, without line breaks. Clients dispatch an event without one as a message.", "type": "string" },
        "id": { "description": "Event id, without line breaks or NUL, which clients send back as Last-Event-ID. An empty id resets it.", "type": "string" },
        "data": { "description": "Text as is; anything else as JSON. A data line per line.", "not": { "type": "null" } },
        "data_json": { "description": "Data as JSON, strings and null too, never a template. In place of data." },
        "retry": { "description": "Reconnection time from this event on.", "$ref": "#/$defs/duration" },
        "comment": { "description": "Sent first, a comment line per line, which clients skip.", "type": "string" },
        "delay": { "description": "How long to wait before the event, in place of event_delay.", "$ref": "#/$defs/duration" },
        "raw": { "description": "Text sent as it is, in place of the other fields but delay.", "type": "string" }
      },
      "additionalProperties": false,
      "not": { "required": [ "data", "data_json" ] },
      "if": { "required": [ "raw" ] },
      "then": { "propertyNames": { "enum": [ "raw", "delay" ] } }
    },
    "on_complete_event": {
      "type": "object",
      "properties": {
        "name": { "description": "Name of a listener registered with Setup().OnCompleteListener. The vlatte command registers webhook.", "type": "string", "examples": [ "webhook" ] },
        "parameters": { },
        "after_response": { "description": "Send the response without waiting for the event, which runs on.", "$ref": "#/$defs/bool" }
      },
      "additionalProperties": false,
      "required": [ "name" ],
      "if": { "properties": { "name": { "const": "webhook" } }, "required": [ "name" ] },
      "then": { "properties": { "parameters": { "$ref": "#/$defs/webhook_parameters" } }, "required": [ "parameters" ] }
    },
    "webhook_parameters": {
      "description": "The request a webhook sends after the mock replies: one of url and url_template, and one body at most.",
      "type": "object",
      "properties": {
        "url": { "description": "An absolute http or https URL. Editors check its scheme and that a host follows; building the mock checks the rest.", "type": "string", "pattern": "^[Hh][Tt][Tt][Pp][Ss]?://[^/?#]" },
        "url_template": { "description": "The URL, as a template.", "type": "string", "minLength": 1 },
        "method": { "description": "The method, in any case. Empty means GET.", "type": "string", "pattern": "^[!#$%&'*+.^_`|~0-9A-Za-z-]*$" },
        "header": { "$ref": "#/$defs/webhook_header" },
        "header_template": { "description": "Headers whose values are templates.", "$ref": "#/$defs/webhook_header" },
        "body": { "type": "string" },
        "body_base64": { "description": "The bytes of the body in standard base64, for a body that isn't UTF-8 text, such as an image. Line breaks are ignored.", "type": "string", "contentEncoding": "base64", "pattern": "^[\\r\\n]*(([A-Za-z0-9+/][\\r\\n]*){4})*(([A-Za-z0-9+/][\\r\\n]*){2}=[\\r\\n]*=|([A-Za-z0-9+/][\\r\\n]*){3}=)?[\\r\\n]*$" },
        "body_template": { "description": "The body, as a template.", "type": "string" },
        "body_file": { "description": "A file to send as the body. Relative paths start at the mock file's directory.", "type": "string" },
        "multipart": { "description": "A multipart body, as response.multipart. It's sent at once, so its parts can't have delays.", "$ref": "#/$defs/multipart" },
        "multipart_template": { "description": "A multipart body whose text is templates.", "$ref": "#/$defs/multipart" },
        "delay": { "description": "How long to wait before sending, once the mock replied. The response waits for it too, unless the event runs after the response.", "$ref": "#/$defs/duration" },
        "timeout": { "description": "How long the request may take, from sending it to its answer. Without it, the listener's timeout, or else 10s.", "$ref": "#/$defs/duration" }
      },
      "additionalProperties": false,
      "oneOf": [ { "required": [ "url" ] }, { "required": [ "url_template" ] } ],
      "allOf": [
        { "not": { "required": [ "body", "body_base64" ] } },
        { "not": { "required": [ "body", "body_template" ] } },
        { "not": { "required": [ "body", "body_file" ] } },
        { "not": { "required": [ "body", "multipart" ] } },
        { "not": { "required": [ "body", "multipart_template" ] } },
        { "not": { "required": [ "body_base64", "body_template" ] } },
        { "not": { "required": [ "body_base64", "body_file" ] } },
        { "not": { "required": [ "body_base64", "multipart" ] } },
        { "not": { "required": [ "body_base64", "multipart_template" ] } },
        { "not": { "required": [ "body_template", "body_file" ] } },
        { "not": { "required": [ "body_template", "multipart" ] } },
        { "not": { "required": [ "body_template", "multipart_template" ] } },
        { "not": { "required": [ "body_file", "multipart" ] } },
        { "not": { "required": [ "body_file", "multipart_template" ] } },
        { "not": { "required": [ "multipart", "multipart_template" ] } },
        {
          "if": { "anyOf": [ { "required": [ "multipart" ] }, { "required": [ "multipart_template" ] } ] },
          "then": {
            "properties": {
              "header": { "propertyNames": { "not": { "pattern": "^[Cc][Oo][Nn][Tt][Ee][Nn][Tt]-[Tt][Yy][Pp][Ee]$" } } },
              "header_template": { "propertyNames": { "not": { "pattern": "^[Cc][Oo][Nn][Tt][Ee][Nn][Tt]-[Tt][Yy][Pp][Ee]$" } } }
            }
          }
        }
      ]
    },
    "webhook_header": {
      "description": "Headers by name, each value a string, or a list of strings for several values. Names ignore case: a name can be set once.",
      "type": "object",
      "additionalProperties": { "oneOf": [ { "type": "string" }, { "type": "array", "items": { "type": "string" } } ] }
    },
    "header_map": {
      "type": "object",
      "patternProperties": {
        "^.*$": { "oneOf": [ {"type": "string"}, {"type": "array", "items": {"type": "string"} } ] }
      }
    }
  }
}
