Kubernetes JSON Manifests: kubectl, JSONPath, Patches & API Objects

Last updated:

Kubernetes stores every resource — Pods, Deployments, Services, ConfigMaps — as JSON in etcd, and the Kubernetes API server communicates exclusively in JSON (or Protobuf for internal traffic), making JSON fluency essential for cluster operations. kubectl get pod my-pod -o json returns a 60+ field JSON object; kubectl get pods -o jsonpath='{.items[*].metadata.name}' extracts just the pod names in under 100 ms without parsing the full JSON in shell. This guide covers Kubernetes API object JSON structure, kubectl JSON output formats, JSONPath queries, strategic merge patches, JSON Patch (RFC 6902) for kubectl patch, admission webhook JSON, and querying etcd JSON directly.

Kubernetes API Object JSON Structure

Every Kubernetes resource JSON has four required top-level fields that form the API object envelope. apiVersion identifies the API group and version (e.g., apps/v1, v1 for core resources). kind names the resource type (Pod, Deployment, Service). metadata contains the object identity and system metadata. spec declares the desired state — ConfigMaps and Secrets use data instead of spec. Understanding this four-field skeleton is the foundation for reading any Kubernetes JSON manifest.

// Four required top-level fields — every Kubernetes resource
{
  "apiVersion": "apps/v1",          // group/version (core group uses just "v1")
  "kind": "Deployment",             // resource type name
  "metadata": {                     // ObjectMeta — identity and system fields
    "name": "my-app",               // required: unique within namespace
    "namespace": "default",         // omit for cluster-scoped resources
    "labels": {                     // key-value pairs for selection and grouping
      "app": "my-app",
      "version": "1.2.0"
    },
    "annotations": {                // arbitrary metadata, not used for selection
      "deployment.kubernetes.io/revision": "3",
      "kubectl.kubernetes.io/last-applied-configuration": "..."
    },
    // Server-populated fields (not in your manifest):
    "uid": "a1b2c3d4-e5f6-...",     // immutable, cluster-unique identifier
    "resourceVersion": "12345",     // etcd revision — used for optimistic locking
    "generation": 3,                // increments on spec changes (not metadata)
    "creationTimestamp": "2026-05-19T10:00:00Z"
  },
  "spec": {                         // desired state — shape varies by kind
    "replicas": 3,
    "selector": { "matchLabels": { "app": "my-app" } },
    "template": { /* PodTemplateSpec */ }
  },
  "status": {                       // current state — populated by controllers
    "replicas": 3,                  // never set this in your manifest
    "readyReplicas": 3,
    "conditions": [
      {
        "type": "Available",
        "status": "True",
        "lastUpdateTime": "2026-05-19T10:01:00Z",
        "reason": "MinimumReplicasAvailable"
      }
    ]
  }
}

// TypeMeta + ObjectMeta + ListMeta — for List responses
{
  "apiVersion": "v1",
  "kind": "PodList",                // List suffix indicates collection
  "metadata": {
    "resourceVersion": "99999",     // ListMeta: cluster-wide resource version
    "continue": ""                  // pagination token (empty = last page)
  },
  "items": [                        // array of Pod objects
    { "apiVersion": "v1", "kind": "Pod", "metadata": { ... }, "spec": { ... } }
  ]
}

// ConfigMap — uses "data" instead of "spec"
{
  "apiVersion": "v1",
  "kind": "ConfigMap",
  "metadata": { "name": "app-config", "namespace": "default" },
  "data": {
    "DATABASE_URL": "postgres://db:5432/mydb",
    "config.json": "{\"debug\": false, \"timeout\": 30}"
  },
  "binaryData": {                   // base64-encoded values for binary content
    "cert.pem": "LS0tLS1CRUdJTi..."
  }
}

The metadata.resourceVersion field is critical for conflict detection: the API server rejects updates where the submitted resourceVersion does not match the current etcd value, implementing optimistic concurrency control. Always include resourceVersion when patching or updating resources programmatically. The metadata.generation field increments only when spec changes — controllers use status.observedGeneration to signal that they have processed the latest spec. The spec/status split is fundamental: users write spec, controllers write status, and the two are updated in separate API calls to avoid race conditions.

kubectl JSON Output: -o json and -o jsonpath

kubectl provides three JSON-related output formats: -o json returns the full API object as pretty-printed JSON, -o jsonpath extracts specific fields using a template expression, and -o custom-columns formats selected JSON fields into a tabular report. Mastering these output formats eliminates the need for external JSON parsers in most shell scripting scenarios. The kubectl explain command documents every JSON field path with its type and description.

# ── -o json: full object ─────────────────────────────────────────
kubectl get pod my-pod -o json
kubectl get pods -o json                         # returns PodList with items[]
kubectl get deployment my-app -o json | jq .spec.template.spec.containers

# ── -o jsonpath: extract specific fields ─────────────────────────
# Single field — curly braces required in Kubernetes JSONPath
kubectl get pod my-pod -o jsonpath='{.spec.nodeName}'

# Multiple fields from a list — items[*] wildcard
kubectl get pods -o jsonpath='{.items[*].metadata.name}'
# Output: my-pod-abc my-pod-def my-pod-xyz  (space-separated)

# Range iterator — newline-separated output
kubectl get pods -o jsonpath='{range .items[*]}{.metadata.name}{"
"}{end}'

# Nested field extraction
kubectl get pods -o jsonpath='{range .items[*]}{.metadata.name}{"	"}{.status.podIP}{"
"}{end}'
# Output:
# my-pod-abc    10.244.1.5
# my-pod-def    10.244.2.3

# Container images for all pods
kubectl get pods -o jsonpath='{.items[*].spec.containers[*].image}'

# Node conditions — nested array access
kubectl get node my-node -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}'

# ── -o jsonpath-file: complex templates ──────────────────────────
# Store template in a file for reuse
cat > pod-template.txt << 'EOF'
{range .items[*]}
Name: {.metadata.name}
IP:   {.status.podIP}
Node: {.spec.nodeName}
{"
"}{end}
EOF
kubectl get pods -o jsonpath-file=pod-template.txt

# ── -o custom-columns: tabular JSON field output ──────────────────
kubectl get pods -o custom-columns='NAME:.metadata.name,IP:.status.podIP,NODE:.spec.nodeName,RESTARTS:.status.containerStatuses[0].restartCount'

# ── --sort-by: sort output by a JSON field ────────────────────────
kubectl get pods --sort-by='{.metadata.creationTimestamp}'
kubectl get pods --sort-by='{.status.containerStatuses[0].restartCount}'

# ── kubectl explain: JSON field documentation ─────────────────────
kubectl explain pod.spec.containers
kubectl explain deployment.spec.template.spec.containers.resources
kubectl explain --recursive pod.spec     # full field tree

The -o jsonpath flag is faster than piping to jq for simple field extraction because it parses the JSON server-side and returns only the requested values. For complex transformations — filtering, arithmetic, conditional logic — jq is more powerful. A useful pattern for CI pipelines: kubectl get deployment my-app -o jsonpath='{.status.readyReplicas}' returns a single integer you can compare directly in a shell conditional without installing jq. See the jq filter examples guide for comparison with jq syntax.

JSONPath Queries for Kubernetes Resources

Kubernetes JSONPath is a subset of the JSONPath standard with several important differences. Expressions must be wrapped in curly braces {} — there is no $ root operator. Filter expressions ([?(@.field==value)]) have limited support: only the ?(@.type=="...") pattern works for conditions arrays. The range keyword iterates arrays. Understanding these constraints prevents frustrating debugging sessions when standard JSONPath syntax silently returns empty output.

# Kubernetes JSONPath vs standard JSONPath — key differences
# Standard JSONPath:  $.items[*].metadata.name
# Kubernetes JSONPath: {.items[*].metadata.name}   (no $, requires {})

# ── Common patterns ───────────────────────────────────────────────

# Pod IPs for all running pods
kubectl get pods -o jsonpath='{.items[*].status.podIP}'

# Container images — nested wildcard
kubectl get pods -o jsonpath='{.items[*].spec.containers[*].image}'

# Node internal IPs
kubectl get nodes -o jsonpath='{.items[*].status.addresses[?(@.type=="InternalIP")].address}'

# Ready condition status for all nodes
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"	"}{.status.conditions[-1].type}{"
"}{end}'

# Service ClusterIPs
kubectl get services -o jsonpath='{range .items[*]}{.metadata.name}{"	"}{.spec.clusterIP}{"
"}{end}'

# All container resource limits across all pods
kubectl get pods -o jsonpath='{range .items[*]}{.metadata.name}{range .spec.containers[*]}{"	"}{.name}{":"}{.resources.limits.memory}{"
"}{end}{end}'

# Deployment rollout status — observedGeneration vs generation
kubectl get deployment my-app -o jsonpath='{.status.observedGeneration}'
kubectl get deployment my-app -o jsonpath='{.metadata.generation}'

# Secret data keys (not values — never log secret values)
kubectl get secret my-secret -o jsonpath='{.data}' | jq 'keys'

# ── Conditional filter — type=="Ready" pattern ────────────────────
# Works: simple equality condition on type field
kubectl get node my-node -o jsonpath='{.status.conditions[?(@.type=="Ready")].status}'
# Returns: "True" or "False"

# ── --sort-by with JSONPath ───────────────────────────────────────
kubectl get pods --sort-by='{.metadata.creationTimestamp}'
kubectl get events --sort-by='{.lastTimestamp}'

# ── Limitations: use jq for these ────────────────────────────────
# No arithmetic:     cannot compute sum of replicas
# No string ops:     cannot split or join strings
# No general filter: [?(@.status.phase=="Running")] may not work
# Workaround: pipe to jq
kubectl get pods -o json | jq '.items[] | select(.status.phase=="Running") | .metadata.name'

When Kubernetes JSONPath returns empty output instead of an error, the expression is syntactically valid but matched nothing — verify the path with kubectl get <resource> -o json | jq first. The [-1] array index (last element) is supported in Kubernetes JSONPath, making .status.conditions[-1] useful for the most recent condition. For production scripts, prefer kubectl get -o json | jq over -o jsonpath for complex queries — jq provides better error messages and supports the full filter expression syntax. See the jq vs JSONPath comparison for detailed syntax differences.

Strategic Merge Patch vs JSON Patch (RFC 6902)

kubectl supports three patch types: --type=strategic (default), --type=merge, and --type=json. Strategic merge patch is the most powerful for Kubernetes because it understands resource semantics — particularly list merging by key. JSON Patch (RFC 6902) is the most precise because it operates atomically on exact paths. Choosing the wrong patch type for list modifications is the most common source of accidental container or port replacement.

# ── Strategic Merge Patch (default) ──────────────────────────────
# Provide a partial object; Kubernetes merges intelligently
# List items are merged by merge key (name for containers, containerPort for ports)

# Scale replicas
kubectl patch deployment my-app -p '{"spec":{"replicas":5}}'

# Add a new container — does NOT replace existing containers (merge key: name)
kubectl patch deployment my-app -p '{
  "spec": {
    "template": {
      "spec": {
        "containers": [
          {
            "name": "sidecar",
            "image": "envoyproxy/envoy:v1.28.0",
            "ports": [{"containerPort": 9901}]
          }
        ]
      }
    }
  }
}'

# Update a specific container image (merge key matches by name)
kubectl patch deployment my-app -p '{
  "spec":{"template":{"spec":{"containers":[{"name":"my-app","image":"my-app:2.0"}]}}}
}'

# ── JSON Merge Patch (RFC 7396) ───────────────────────────────────
# Simpler than strategic but replaces arrays entirely — use with caution
kubectl patch deployment my-app --type=merge -p '{"spec":{"replicas":5}}'
# WARNING: patching containers[] with merge replaces ALL containers
# Only use merge patch for scalar fields and maps, not arrays

# ── JSON Patch (RFC 6902) ─────────────────────────────────────────
# Array of operation objects — surgical precision

# Replace scalar — scale deployment
kubectl patch deployment my-app --type=json   -p '[{"op":"replace","path":"/spec/replicas","value":3}]'

# Add env var to first container (index 0)
kubectl patch deployment my-app --type=json -p '[
  {
    "op": "add",
    "path": "/spec/template/spec/containers/0/env/-",
    "value": {"name": "LOG_LEVEL", "value": "debug"}
  }
]'

# Remove a label
kubectl patch pod my-pod --type=json   -p '[{"op":"remove","path":"/metadata/labels/env"}]'

# Move — rename an annotation key
kubectl patch pod my-pod --type=json -p '[
  {"op":"move","from":"/metadata/annotations/old-key","path":"/metadata/annotations/new-key"}
]'

# Test + replace — optimistic concurrency (atomic check-and-set)
kubectl patch deployment my-app --type=json -p '[
  {"op":"test",    "path":"/spec/replicas","value":1},
  {"op":"replace", "path":"/spec/replicas","value":3}
]'
# Fails with "test operation failed" if replicas is not currently 1

# ── Patch type comparison ─────────────────────────────────────────
# strategic  — Kubernetes-aware list merging; best for manifests
# merge      — RFC 7396; replaces arrays; safe for scalar/map fields only
# json       — RFC 6902; precise operations; best for atomic updates

The test operation in JSON Patch is underused but powerful — it makes a patch atomic by failing the entire operation if the tested value does not match. This implements check-and-set semantics without a separate read-modify-write cycle. For Kubernetes operators and controllers, JSON Patch with test is safer than strategic merge patch when modifying fields that other controllers may also update concurrently. See the JSON merge patch guide and JSON Patch (RFC 6902) reference for detailed operation coverage.

ConfigMaps and Secrets as JSON

ConfigMaps are the primary mechanism for injecting configuration JSON into Kubernetes workloads. The data field holds UTF-8 string values; binaryData holds base64-encoded binary values. A common pattern stores an entire JSON configuration file as a single ConfigMap key, then mounts it as a file in the container. Understanding the JSON structure of ConfigMaps and Secrets enables precise programmatic management via the Kubernetes API.

# ── ConfigMap JSON structure ──────────────────────────────────────
{
  "apiVersion": "v1",
  "kind": "ConfigMap",
  "metadata": { "name": "app-config", "namespace": "default" },
  "data": {
    // Simple string key-value pairs
    "DATABASE_HOST": "postgres.default.svc.cluster.local",
    "DATABASE_PORT": "5432",
    "LOG_LEVEL": "info",
    // Entire JSON file as a string value (key = filename)
    "app.json": "{\"debug\": false, \"timeout\": 30, \"features\": [\"auth\", \"cache\"]}"
  },
  "binaryData": {
    // base64-encoded binary content (certificates, fonts, etc.)
    "ca.crt": "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0t..."
  }
}

# ── Create ConfigMap from a JSON file ────────────────────────────
kubectl create configmap app-config --from-file=app.json=./config/app.json
kubectl create configmap app-config --from-file=./config/    # all files in dir
kubectl create configmap app-config --from-literal=KEY=value

# ── Secret JSON structure ─────────────────────────────────────────
{
  "apiVersion": "v1",
  "kind": "Secret",
  "metadata": { "name": "app-secrets", "namespace": "default" },
  "type": "Opaque",
  "data": {
    // All values MUST be base64-encoded
    "DATABASE_PASSWORD": "c3VwZXJzZWNyZXQ=",   // base64("supersecret")
    "API_KEY": "bXlhcGlrZXk="                   // base64("myapikey")
  },
  "stringData": {
    // Convenience field: plain text, auto-base64-encoded on write
    // stringData is write-only — kubectl get returns base64 in .data
    "REDIS_URL": "redis://redis:6379/0"
  }
}

# ── Referencing ConfigMap JSON in a Pod ───────────────────────────
{
  "apiVersion": "v1",
  "kind": "Pod",
  "spec": {
    "containers": [{
      "name": "my-app",
      "image": "my-app:1.0",
      "envFrom": [
        { "configMapRef": { "name": "app-config" } },    // all keys as env vars
        { "secretRef": { "name": "app-secrets" } }
      ],
      "env": [
        {
          "name": "SPECIFIC_KEY",                        // single key injection
          "valueFrom": { "configMapKeyRef": { "name": "app-config", "key": "LOG_LEVEL" } }
        }
      ],
      "volumeMounts": [
        { "name": "config-vol", "mountPath": "/etc/app", "readOnly": true }
      ]
    }],
    "volumes": [
      {
        "name": "config-vol",
        "configMap": {
          "name": "app-config",
          "items": [
            { "key": "app.json", "path": "app.json" }   // mounts as /etc/app/app.json
          ]
        }
      }
    ]
  }
}

A critical distinction: Secret.data values must be base64-encoded in the JSON/YAML manifest, but Kubernetes automatically base64-decodes them when injecting into Pods as environment variables or volume files — the container sees plain text. The stringData field accepts plain text and is encoded automatically on write, but kubectl get secret -o json always returns data (base64) — stringData is never returned. To decode a secret value: kubectl get secret my-secret -o jsonpath='{.data.API_KEY}' | base64 --decode.

Admission Webhooks and JSON AdmissionReview

Admission webhooks are HTTP servers that intercept Kubernetes API requests before objects are persisted to etcd. The API server sends an AdmissionReview JSON object to the webhook and expects an AdmissionReview JSON response. Mutating webhooks can modify the object by returning a JSON Patch; validating webhooks can accept or reject it. Understanding the AdmissionReview JSON structure is essential for writing policy enforcement tools and resource mutation logic.

// ── AdmissionReview request — sent by API server to webhook ──────
{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "request": {
    "uid": "705ab4f5-6393-11e8-b7cc-42010a800002",  // echo in response
    "kind": { "group": "apps", "version": "v1", "kind": "Deployment" },
    "resource": { "group": "apps", "version": "v1", "resource": "deployments" },
    "namespace": "default",
    "operation": "CREATE",             // CREATE | UPDATE | DELETE | CONNECT
    "userInfo": {
      "username": "alice",
      "groups": ["system:authenticated"],
      "uid": "user-uid-123"
    },
    "object": {                        // full JSON of the object being created
      "apiVersion": "apps/v1",
      "kind": "Deployment",
      "metadata": { "name": "my-app", "namespace": "default" },
      "spec": { "replicas": 1, ... }
    },
    "oldObject": null,                 // populated on UPDATE (previous state)
    "dryRun": false                    // true if kubectl apply --dry-run=server
  }
}

// ── Mutating webhook response — allowed + JSON Patch ─────────────
{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "response": {
    "uid": "705ab4f5-6393-11e8-b7cc-42010a800002",  // must match request.uid
    "allowed": true,
    "patchType": "JSONPatch",
    // base64-encoded JSON Patch array
    "patch": "W3sib3AiOiJhZGQiLCJwYXRoIjoiL3NwZWMvdGVtcGxhdGUvc3BlYy9jb250YWluZXJzLy0iLCJ2YWx1ZSI6eyJuYW1lIjoiaW5pdC1jb250YWluZXIiLCJpbWFnZSI6ImluaXQ6bGF0ZXN0In19XQ=="
    // Decoded patch: [{"op":"add","path":"/spec/template/spec/containers/-","value":{"name":"sidecar","image":"envoy:latest"}}]
  }
}

// ── Validating webhook response — denied with reason ─────────────
{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  "response": {
    "uid": "705ab4f5-6393-11e8-b7cc-42010a800002",
    "allowed": false,
    "status": {
      "code": 403,
      "message": "Deployment must have resource limits set on all containers"
    }
  }
}

// ── Webhook server in Node.js — parsing and responding ────────────
import express from 'express'
import { Buffer } from 'buffer'

const app = express()
app.use(express.json())

app.post('/mutate', (req, res) => {
  const review = req.body
  const uid = review.request.uid
  const deployment = review.request.object

  // Build JSON Patch — add default resource limits if missing
  const patch = []
  const containers = deployment.spec?.template?.spec?.containers ?? []
  containers.forEach((c: { resources?: { limits?: unknown } }, i: number) => {
    if (!c.resources?.limits) {
      patch.push({
        op: 'add',
        path: `/spec/template/spec/containers/${i}/resources`,
        value: { limits: { cpu: '500m', memory: '256Mi' } }
      })
    }
  })

  res.json({
    apiVersion: 'admission.k8s.io/v1',
    kind: 'AdmissionReview',
    response: {
      uid,
      allowed: true,
      ...(patch.length > 0 && {
        patchType: 'JSONPatch',
        patch: Buffer.from(JSON.stringify(patch)).toString('base64'),
      }),
    },
  })
})

The webhook timeout defaults to 10 seconds (configurable in MutatingWebhookConfiguration.webhooks[].timeoutSeconds, max 30 seconds). If the webhook does not respond within this window, the failurePolicy determines the outcome: Fail (reject the request — safe but disruptive) or Ignore (allow the request — risky if the mutation is required for correctness). Always set failurePolicy: Fail for security-critical validating webhooks and use namespaceSelector to exclude the webhook namespace itself from interception, preventing deadlocks during webhook Pod restarts.

Custom Resource Definitions (CRDs) and JSON Schema Validation

Custom Resource Definitions extend the Kubernetes API with new resource types, and their instances are validated against an OpenAPI v3 schema stored in the CRD manifest. This schema is a JSON Schema dialect that Kubernetes uses both for admission-time validation and for pruning (removing unknown fields). Understanding CRD schema structure enables you to write robust custom resources with precise field constraints and helpful error messages.

// ── CRD manifest with OpenAPI v3 schema validation ───────────────
{
  "apiVersion": "apiextensions.k8s.io/v1",
  "kind": "CustomResourceDefinition",
  "metadata": { "name": "databases.example.com" },
  "spec": {
    "group": "example.com",
    "names": { "kind": "Database", "plural": "databases", "singular": "database" },
    "scope": "Namespaced",
    "versions": [
      {
        "name": "v1",
        "served": true,
        "storage": true,
        "schema": {
          "openAPIV3Schema": {
            "type": "object",
            "properties": {
              "spec": {
                "type": "object",
                "required": ["engine", "storage"],
                "properties": {
                  "engine": {
                    "type": "string",
                    "enum": ["postgres", "mysql", "sqlite"],
                    "description": "Database engine to use"
                  },
                  "version": {
                    "type": "string",
                    "pattern": "^\\d+\\.\\d+$",
                    "description": "Engine version in major.minor format"
                  },
                  "storage": {
                    "type": "object",
                    "required": ["size"],
                    "properties": {
                      "size": { "type": "string" },             // e.g., "10Gi"
                      "storageClass": { "type": "string" }
                    }
                  },
                  "replicas": {
                    "type": "integer",
                    "minimum": 1,
                    "maximum": 5,
                    "default": 1
                  },
                  "config": {
                    "type": "object",
                    // Allow arbitrary JSON in config — disable pruning
                    "x-kubernetes-preserve-unknown-fields": true
                  }
                }
              },
              "status": {
                "type": "object",
                "x-kubernetes-preserve-unknown-fields": true   // controllers write status
              }
            }
          }
        }
      }
    ]
  }
}

// ── Valid custom resource instance ────────────────────────────────
{
  "apiVersion": "example.com/v1",
  "kind": "Database",
  "metadata": { "name": "my-db", "namespace": "default" },
  "spec": {
    "engine": "postgres",
    "version": "16.2",
    "storage": { "size": "20Gi", "storageClass": "fast-ssd" },
    "replicas": 3,
    "config": { "max_connections": 200, "shared_buffers": "256MB" }
  }
}

// ── Validation error examples ─────────────────────────────────────
// Missing required field: spec.storage
//   Error: spec.storage in body is required

// Invalid enum value:
//   spec.engine: Unsupported value: "oracle"; supported values: postgres, mysql, sqlite

// Pattern mismatch:
//   spec.version: Does not match pattern '^\d+\.\d+$'

// ── Test validation without applying ─────────────────────────────
kubectl apply --dry-run=server -f database.yaml
kubectl apply --dry-run=client -f database.yaml   // client-side, less thorough

// ── View CRD schema ───────────────────────────────────────────────
kubectl explain database.spec
kubectl explain database.spec.storage
kubectl get crd databases.example.com -o jsonpath='{.spec.versions[0].schema.openAPIV3Schema}'

Kubernetes CRD validation uses structural schemas — a subset of OpenAPI v3 that requires all fields to be declared at every level (x-kubernetes-preserve-unknown-fields: true opts out of this for specific subtrees). Structural schemas enable Kubernetes to prune unknown fields from custom resources, preventing schema drift over time. The default keyword in CRD schemas sets field defaults at admission time — the API server applies defaults before storing the object in etcd, so the stored JSON always includes default values. Use kubectl explain on custom resources the same way as built-in resources — CRD schema descriptions appear in the explain output.

Key Terms

apiVersion
The top-level JSON field identifying the Kubernetes API group and version for a resource. Core resources (Pod, Service, ConfigMap, Namespace) use just the version string ("v1"). Extended resources use a group/version format: "apps/v1" for Deployments, "networking.k8s.io/v1" for Ingress, "example.com/v1" for custom resources. The API server uses apiVersion and kind together to identify the correct REST endpoint, schema, and storage backend. Changing the apiVersion of a resource to a deprecated version does not affect the stored object — the API server converts between versions transparently using conversion webhooks or built-in conversion logic.
strategic merge patch
A Kubernetes extension of JSON Merge Patch (RFC 7396) that understands the semantics of Kubernetes resource fields, particularly list merging. Standard merge patch replaces arrays entirely when a patch contains an array key — strategic merge patch instead merges list items by a declared merge key. For spec.containers, the merge key is name: patching with a container named sidecar adds it if absent or updates it if present, without disturbing other containers. Merge keys are annotated in the Kubernetes Go source code with the patchMergeKey struct tag. Strategic merge patch also supports the $patch: delete directive to remove specific list items by merge key. It is the default patch type for kubectl patch and kubectl apply.
JSON Patch
An IETF standard (RFC 6902) for describing changes to a JSON document as an array of operation objects. Each operation has an op field (add, remove, replace, move, copy, or test), a path field (a JSON Pointer per RFC 6901 using /-separated segments), and a value field for operations that set values. Operations are applied atomically — if any operation fails (including a test assertion), the entire patch is rejected and the document is unchanged. In Kubernetes, JSON Patch is used with kubectl patch --type=json and in admission webhook mutation responses (base64-encoded in response.patch).
admission webhook
An HTTP server registered with Kubernetes via a MutatingWebhookConfiguration or ValidatingWebhookConfiguration resource that intercepts API requests before they are persisted to etcd. The API server sends an AdmissionReview JSON object to the webhook's HTTPS endpoint; the webhook must respond with an AdmissionReview JSON object containing the response. Mutating webhooks can modify the object by returning a base64-encoded JSON Patch in response.patch; validating webhooks can only accept (allowed: true) or reject (allowed: false) the request. Both types are invoked after authentication and authorization but before the object is written to etcd. The failurePolicy field controls behavior when the webhook is unreachable.
etcd
A distributed, strongly-consistent key-value store used by Kubernetes as its primary backing store for all cluster state. Every Kubernetes resource — Pods, Deployments, Services, Secrets — is stored in etcd under a /registry/<group>/<resource>/<namespace>/<name> key path. Despite the Kubernetes API being JSON-based, etcd stores objects as Protobuf by default (since Kubernetes 1.7) for compactness — the API server handles serialization/deserialization. etcd's watch mechanism powers Kubernetes controllers: controllers establish watch connections to the API server, which translates etcd change events into JSON or Protobuf watch events. etcd uses the Raft consensus algorithm to replicate writes across its cluster, providing strong consistency guarantees that enable Kubernetes' optimistic concurrency control via resourceVersion.
CRD (Custom Resource Definition)
A Kubernetes extension mechanism that registers new resource types with the API server without modifying Kubernetes source code. A CRD manifest defines the group, kind, plural name, scope (Namespaced or Cluster), API versions, and an OpenAPI v3 JSON schema for validation. Once a CRD is applied, the API server accepts, validates, stores, and serves instances of the custom resource as JSON objects with the same four-field structure (apiVersion, kind, metadata, spec) as built-in resources. CRDs are the foundation of the Kubernetes operator pattern: a custom controller watches for CRD instances and reconciles cluster state to match the declared spec. The OpenAPI v3 schema in the CRD enables field validation, pruning of unknown fields, and kubectl explain documentation.

FAQ

How do I get a Kubernetes resource as JSON with kubectl?

Run kubectl get <resource> <name> -o json to retrieve the full JSON representation of any Kubernetes object. For example, kubectl get pod my-pod -o json returns a complete JSON object with all fields including metadata, spec, and status. To get all objects of a type, omit the name: kubectl get pods -o json returns a List object with an items array containing each pod. Pipe to jq for further processing: kubectl get pod my-pod -o json | jq .spec.containers[0].image. Save to a file with: kubectl get deployment my-app -o json > my-app.json. The -o json flag works with any resource type including nodes, services, configmaps, secrets, and custom resources. Note that the JSON output includes server-side fields like resourceVersion, uid, and status that are not present in your original manifest YAML.

How do I use JSONPath with kubectl to query specific fields?

Use kubectl get <resource> -o jsonpath='<template>' where the template is a JSONPath expression wrapped in curly braces. For example, kubectl get pod my-pod -o jsonpath='{.spec.nodeName}' returns just the node name. For lists, use the wildcard: kubectl get pods -o jsonpath='{.items[*].metadata.name}' returns all pod names space-separated. Use range to iterate: kubectl get pods -o jsonpath='{range .items[*]}{.metadata.name}{" "}{end}' prints each name on its own line. Store complex templates in a file with -o jsonpath-file=template.txt. Kubernetes JSONPath differs from standard JSONPath: expressions must be wrapped in curly braces {}, there is no $ root operator, and most filter expressions are unsupported — use --field-selector instead. Sort results with --sort-by='{.metadata.name}'.

What is the difference between JSON Patch and strategic merge patch in Kubernetes?

JSON Patch (RFC 6902) describes changes as an array of operation objects — each with op (add/remove/replace/move/copy/test), path (a JSON Pointer), and value. It is exact and surgical: use kubectl patch --type=json. Strategic merge patch is a Kubernetes extension that understands resource semantics. You provide a partial object and Kubernetes merges it intelligently — for lists, it uses merge keys (the name field for containers) to merge list items by identity rather than replacing the entire list. This makes it safe to add a container to a Deployment without knowing the other containers. Use JSON Patch for precise atomic updates with the test operation for safety; use strategic merge patch for additive changes to complex objects.

How do I patch a Kubernetes resource using JSON Patch?

Use kubectl patch with --type=json and a -p flag containing a JSON array of RFC 6902 operations. Scale a deployment: kubectl patch deployment my-app --type=json -p '[{"op":"replace","path":"/spec/replicas","value":3}]'. Add an env var: -p '[{"op":"add","path":"/spec/template/spec/containers/0/env/-","value":{"name":"LOG_LEVEL","value":"debug"}}]'. Remove a label: kubectl patch pod my-pod --type=json -p '[{"op":"remove","path":"/metadata/labels/env"}]'. The - at the end of an array path appends to the array. Integer indexes (0, 1) address specific array elements. Use the test operation before a replace to implement optimistic concurrency — the patch fails if the tested value does not match, preventing accidental overwrites in concurrent environments.

How do Kubernetes admission webhooks use JSON?

The API server sends an AdmissionReview JSON object to the webhook's HTTPS endpoint. The request field contains: uid (must be echoed in the response), object (the full JSON of the resource), oldObject (previous state for UPDATE), operation (CREATE/UPDATE/DELETE/CONNECT), and userInfo (identity of the requesting user). A mutating webhook responds with allowed: true, patchType: "JSONPatch", and patch — a base64-encoded JSON array of RFC 6902 operations that modify the object before it is stored. A validating webhook responds with allowed: true or false, with an optional status.message for denials. The webhook must respond within the timeout (default 10 seconds). The failurePolicy on the webhook configuration controls whether timeout failures cause the request to be rejected (Fail) or allowed through (Ignore).

How do I create a ConfigMap from a JSON file?

Use kubectl create configmap <name> --from-file=<key>=<file.json> to create a ConfigMap where the entire JSON file content becomes the value of a single key. For example, kubectl create configmap app-config --from-file=config.json=./config/app.json creates a ConfigMap with one key named config.json whose value is the raw JSON string. To use a directory of files, omit the key: --from-file=./config/ creates one key per file, using the filename as the key. Mount the ConfigMap as a volume in a Pod to make the JSON file available at a path: set volumes[].configMap.items[].key to config.json and path to app.json to mount it at /etc/app/app.json. The application reads the JSON file normally at runtime. Preview the generated ConfigMap without applying with kubectl create configmap ... --dry-run=client -o yaml.

What JSON schema validation does Kubernetes support for custom resources?

Kubernetes validates Custom Resource instances against an OpenAPI v3 schema in the CRD's spec.versions[].schema.openAPIV3Schema field. Supported JSON Schema keywords include: type, properties, required, enum, minimum, maximum, minLength, maxLength, pattern, items (for arrays), additionalProperties, and default. Kubernetes adds extensions: x-kubernetes-preserve-unknown-fields: true disables pruning for a subtree (allowing arbitrary JSON); x-kubernetes-int-or-string: true allows a field to accept integers or strings; x-kubernetes-embedded-resource: true marks an embedded Kubernetes object. Validation runs at admission time — errors are returned immediately on kubectl apply. Test without applying: kubectl apply --dry-run=server -f resource.yaml. View CRD field documentation with kubectl explain <crd-kind>.spec.

How does Kubernetes store resources internally?

Kubernetes stores all resources in etcd under keys following the pattern /registry/<group>/<resource>/<namespace>/<name> — for example, /registry/pods/default/my-pod. Despite JSON being the API wire format, Kubernetes stores objects in etcd as Protobuf by default (since 1.7) for compactness and speed — Protobuf encoding is roughly 30% smaller and faster than JSON. The API server handles all serialization: clients speak JSON (or Protobuf) to the API server, which converts to/from the etcd storage format. To inspect etcd data directly: etcdctl get /registry/pods/default/my-pod --print-value-only returns binary Protobuf with a k8s\x00magic prefix. etcd's watch mechanism drives Kubernetes controllers — a controller establishes a Watch connection to the API server, which translates etcd change events into JSON watch events, enabling reactive reconciliation without polling.

Further reading and primary sources