Skip to content
Guides

5 JSON Schema readOnly and writeOnly Tests Before Release

·9 min read
5 JSON Schema readOnly and writeOnly Tests Before Release - Guides guide from JSON Prettifier

Quick answer: Test JSON Schema field direction at five boundaries: annotation behavior, request acceptance, response construction, nested data, and endpoint execution. A readOnly or writeOnly label does not replace an enforced application policy. These examples use Draft 2020-12 and explicit request and response schemas so acceptance and omission are directly testable.

A generated form hides the account identifier, yet a hand-written request can still send it. A password input looks protected, yet a generic serializer returns a private field. Both failures can exist while the shared model schema validates successfully.

The JSON Schema introduction covers structural validation. This guide focuses on the direction in which fields travel. Wikipedia’s encapsulation overview supplies background on separating internal representation from public interfaces; the linked specifications and tests establish the behavior below.

1. Does ordinary validation treat field direction as annotation rather than automatic enforcement?

Test a schema containing readOnly and writeOnly with an ordinary validator before relying on framework behavior. A matching instance can remain valid with both annotated properties present, and required still checks presence. Record this baseline so a generated form, SDK, or API adapter is not mistaken for the underlying schema evaluator’s universal behavior.

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "type": "object",
  "properties": {
    "id": {"type": "integer", "readOnly": true},
    "email": {"type": "string"},
    "secret": {"type": "string", "writeOnly": true}
  },
  "required": ["id", "email", "secret"],
  "additionalProperties": false
}

With ordinary Draft 2020-12 evaluation, {"id":7,"email":"reader@example.test","secret":"fixture-only"} passes this schema. Removing id fails required. Removing secret fails required too. The evaluator is checking one instance; it has not been told to transform the required set according to an HTTP operation.

The official annotations reference and validation specification describe these direction keywords. An API framework can interpret them when generating or evaluating operation-specific contracts, but that additional behavior needs its own tests. Do not extrapolate from one framework to a separate validator used in a queue consumer or CLI.

A useful baseline fixture deliberately includes a server-owned field. If your test expects universal rejection, the failure reveals a mistaken expectation before deployment. Then test the framework’s directional mode separately with its configuration recorded. A passing form preview proves that the interface is rendered a certain way, not that the server prevents an edited request from changing protected state.

Keep contract generation under test too. If the request and response schemas are generated from one source model, save representative generated artifacts and run fixtures against those outputs. A test of the source model can pass while the generator leaves a required response identifier in the create-request schema. Review generated changes by operation, rather than accepting a schema snapshot merely because a code generator produced it.

Keep the fixture secret artificial. The test demonstrates property presence and output policy, not password storage or authentication design. No real credential is needed to establish whether an annotation changes ordinary schema validity.

2. Does the request contract reject server-owned fields before model assignment?

Create an explicit request contract that permits client-editable fields and rejects protected fields. Test a valid request, a request carrying a server-owned identifier, and an unrelated extra property. Confirm that rejected data never reaches model assignment, because a hidden form control or direction annotation cannot prevent a caller from constructing a different JSON body.

{
  "type": "object",
  "properties": {"email": {"type": "string"}, "secret": {"type": "string"}},
  "required": ["email", "secret"],
  "additionalProperties": false
}

This illustrative create-request schema accepts the email and fixture secret. It rejects the same request with id added. It also rejects an invented isAdmin property. Both negatives matter: blocking one known field is weaker than defining the complete set of assignable input fields for this operation.

The OWASP mass-assignment guidance recommends allow-listing bindable fields or using dedicated data transfer objects. A narrow request schema and a narrow application mapping can support that boundary. They do not replace the caller’s authorization check, which decides whether this caller may perform this operation on this resource.

Choose rejection or documented ignoring deliberately. This example rejects unknown input rather than silently deleting it. If your compatibility policy intentionally ignores unknown fields, verify that ignored data cannot later be assigned by a downstream component that still holds the original object. Keeping an unvalidated raw dictionary beside a sanitized one is a common source of boundary confusion.

Create, replace, and partial-update requests can have different required sets. A create request might require a secret while a profile update permits only email. Do not reuse the create schema for every route merely because the model is shared. The object closure tests explain how to keep unknown-field handling correct when schemas are composed.

3. Does response construction omit private fields before the body is sent?

Construct the public response from an explicit field projection and validate that result against a response contract. Test an internal record containing private data, a newly added internal field, and a deliberately unfiltered response. A writeOnly annotation alone does not remove data, and output validation helps only when failure prevents the invalid body from reaching the client.

from copy import deepcopy

def public_account(record):
    return deepcopy({"id": record["id"], "email": record["email"]})

A matching response schema declares id and email, requires both, and rejects additional properties. The projection omits secret even when the internal record contains it. It also omits an unrelated internalNote without needing a new blacklist entry. This makes the public shape independent of the growing set of internal model attributes.

Test the deliberately unsafe alternative too: validating the full internal record against the strict response schema should fail. That fixture proves the output gate can catch a regression if someone later replaces the projection with a generic model-to-dictionary conversion. It does not prove that the gate is wired into the live endpoint; the final check covers that integration.

Use JSON Compare to inspect sanitized internal and public fixtures when review reveals an unexpected structural change. It helps show additions and removals; it does not enforce the schema or understand which field is sensitive. Keep the expected public object in automated assertions so correctness does not depend on a manual comparison session.

For each operation, keep a small table of expected input fields and expected output fields in the test description. Include server-generated values, client-editable values, private internal values, and fields whose visibility depends on caller permissions. The schema test should assert the shape for the relevant permission context; a separate permission test should establish why that context was selected. This prevents a privileged response shape from becoming the default representation for ordinary callers.

Test error responses and alternate success paths as well. A safe normal response does not establish that validation errors, debug details, export endpoints, or asynchronous callbacks use the same public representation. Name the tested surface precisely and add fixtures for each path that can serialize the account’s internal data.

4. Do nested objects and arrays obey the same direction policy?

Repeat field-direction tests at every relevant nested object and array element rather than assuming a top-level policy propagates automatically. Define explicit public shapes for nested records, then test one sensitive nested field and one unexpected field in a later array element. This catches shallow projections and validators that inspect only the first apparently valid item.

For example, an internal account can contain profile and a list of sessions. The public profile might expose displayName while omitting privateNote. A public session might expose createdAt while omitting token. A top-level whitelist that simply copies the entire profile object or sessions array does not establish either nested policy.

def public_profile(record):
    return {
        "profile": {"displayName": record["profile"]["displayName"]},
        "sessions": [{"createdAt": s["createdAt"]} for s in record["sessions"]],
    }

The accompanying response schema should constrain the profile object and use items to constrain every session object. In this example, each relevant object rejects additional properties. Test an empty session list, two valid public sessions, and a private token inserted into the second public session. The last fixture proves the rule is applied beyond index zero.

Keep optional and null behavior explicit. If profile can be absent or null in the internal model, the serializer must define its public representation rather than failing unexpectedly or copying the raw value. Test the allowed variants with their matching schema. The null representation guide helps distinguish absence from an explicit null when designing that contract.

Schema definitions can be shared where the actual public contract is shared. Avoid a universal recursive “remove writeOnly” walker that guesses which branches or references apply. Composition and context can make annotation interpretation more involved than a key lookup. An explicit projection is easier to audit for a small API; larger systems need a tested directional serializer with clearly defined semantics.

5. Does the endpoint enforce both input and output boundaries during execution?

Run accepted and rejected fixtures through the actual request adapter, authorization layer, handler, and response serializer. Assert that invalid input cannot execute the operation and invalid output cannot be sent. Record the selected schema dialect and framework options so the same request and response policies survive dependency upgrades and are not inferred only from generated documentation.

This small Python fixture uses python-jsonschema’s validator interface to test the projection independently. It is a focused unit example, not a complete HTTP server. The integration suite must additionally confirm that the real endpoint invokes equivalent input and output gates at the intended time.

from jsonschema import Draft202012Validator

response_schema = {
    "type": "object",
    "properties": {"id": {"type": "integer"}, "email": {"type": "string"}},
    "required": ["id", "email"],
    "additionalProperties": False,
}
Draft202012Validator.check_schema(response_schema)
validator = Draft202012Validator(response_schema)
internal = {"id": 7, "email": "reader@example.test",
            "secret": "fixture-only", "internalNote": "private-fixture"}
before = deepcopy(internal)
public = public_account(internal)
assert public == {"id": 7, "email": "reader@example.test"}
assert validator.is_valid(public)
assert not validator.is_valid(internal)
assert internal == before

For the request boundary, count handler invocations: an input containing id or isAdmin should not call the operation in the rejecting policy shown above. For the output boundary, force a faulty serializer to return secret and assert that the response gate blocks transmission. Logging an output validation failure while still sending the body defeats the purpose of that gate.

Check the serialized wire body, not only the Python object returned by a helper. Framework response hooks can merge fields back into the object, wrap it in another envelope, or choose a different serializer for errors. Parse the response text in the integration test and assert the complete permitted shape before checking individual values. Retain an explicit negative assertion for the private fixture field so an apparently harmless response-wrapper change cannot escape review.

Finally, test caller permissions independently. An editable email field is not permission to edit any account’s email. Direction, shape, resource ownership, and operation authorization are separate questions. The JSON security guide covers broader safeguards; this release checklist establishes the narrower, observable contract for which fields may enter and leave each tested API operation.

Frequently Asked Questions

Does readOnly make a property invalid in every request?
The readOnly keyword is an annotation describing intended use, not a universal request-rejection assertion in ordinary JSON Schema validation. A framework can add direction-aware behavior. Test that behavior explicitly, or use a request schema that excludes protected fields and an application mapping that cannot assign them from untrusted input during an update.
Does writeOnly automatically remove a field from JSON output?
No. A writeOnly annotation does not automatically redact a serialized response. Your framework or application must apply an output policy. Test the actual response body, including nested values and error paths, and prefer an explicit public-field projection so adding a private model field does not silently add it to the API representation.
Does required ignore a readOnly or writeOnly annotation?
In ordinary JSON Schema validation, required remains a presence assertion even when a property carries a direction annotation. It does not independently know whether the instance is a request or response. Use explicit directional contracts or verify the additional semantics supplied by your API framework rather than assuming that one shared schema covers every operation.
Can a response schema stop sensitive data from leaking?
A response schema can detect a forbidden field when the serialized body is validated before transmission and failure prevents sending it. The schema does not itself construct a safe response or remove private values. Pair a deliberate serializer with response validation, and test that failed output checks block the body rather than merely logging a warning.
Can JSON Compare test readOnly and writeOnly behavior?
JSON Compare can help inspect differences between sanitized internal and public JSON fixtures. It does not enforce field permissions, apply direction annotations, or certify that a response is safe. Use executable request and response assertions for enforcement, then use structural comparison to investigate unexpected additions, removals, or nested changes during review.

Working with JSON right now?

Paste it into JSON Prettifier to format, validate, or minify it. Everything runs in your browser.

Open JSON Prettifier