Audit snapshots and API serialization ============================================================ Use ``sf_toolkit.serialize_snapshot`` for private audit/storage documents, not for Salesforce write payloads. It recursively retains declared readonly fields (including IDs), nested records/lists, and captured unknown response fields. Normal ``serialize_object`` remains the write-oriented formatter: it excludes readonly/unknown fields by default, including nested typed list records. .. code-block:: python import json from sf_toolkit import serialize_snapshot document = serialize_snapshot(deployment_result) private_audit_json = json.dumps(document) known_fields_only = serialize_snapshot(deployment_result, include_unknown=False) Snapshots reuse scalar field formatting. Dates/times become ISO strings, multipicklists become semicolon-delimited strings, and null stays JSON null. Nested references and list records use snapshot formatting recursively rather than write formatting, so readonly IDs and future metadata fields are not lost. Declared fields win collisions with unknown audit fields. Shared references are serialized at each location; cycles and depth beyond ``max_depth`` (default 100) raise a fixed ``ValueError``. Non-finite numbers, non-string mapping keys, opaque objects and binary fields require explicit handling. This formatting applies to hydrated models, whether constructed directly or returned by model-based API helpers. A parsed JSON dictionary/list (including a raw Metadata response) stays a JSON tree: snapshots do not inject schema fields or conflate absent keys with null. Snapshot formatting is not a substitute for keeping the raw response when exact parsed shapes matter. The serializer does not inspect arbitrary object internals or read blob files. It is **not a redaction layer**: declared/captured data can contain secrets or PII. Keep snapshots private, and never pass them as API mutation payloads or unrestricted action output. Unknown fields captured by non-strict models are audit data, not permission to write new Salesforce fields. Strict models continue rejecting undeclared constructor fields. Metadata models retain future provider fields in ``additional_fields`` and expose them to explicit snapshots without adding them to write serialization. The known deployment statuses include ``Queued``, ``FinalizingDeploy`` and ``FinalizingDeployFailed``. In the default hydrated models, ``done`` and ``success`` accept JSON booleans or null, not truthy strings/numbers. Flow coverage results/warnings are retained in ``RunTestsResult``. These types do not replace application-specific deployment policy or a credential-safe error boundary. ``ListField(ChildModel)`` automatically hydrates dictionary members into declared model instances, including non-SObject models. Nested field validation runs during deserialization, not only on a later write. Undeclared data inside each model is retained without guessing its type. ``typed_items=False`` explicitly leaves a field's list members untyped; ``RawField`` is a schema-less field alternative. For a complete raw Metadata response, use ``typed=False``. API-write formatting still applies the declared member schema and excludes readonly/unknown fields; snapshot formatting retains readonly/unknown fields recursively. Populated binary declarations on either a regular model or an SObject require explicit handling, even when SObject's API-oriented field map hides the blob. Snapshots reject them without opening/reading content. Attribute reassignment of already-captured unknown fields updates their audit store; arbitrary new implementation/private attributes are not automatically reflected into snapshots. Multipicklist values ------------------------------------------------------------ ``MultiPicklistField`` preserves null and empty string distinctly, and its formatter uses the current ``MultiPicklistValue.values`` selections rather than the str subclass's original bytes. Selection validation also runs during formatting, catching invalid in-place edits. .. code-block:: python record.Tags.values[:] = ["First", "Second"] # Descriptor assignment is needed to mark in-place edits dirty for an # only_changes=True write; this also applies to other mutable field values. record.Tags = record.Tags Use the field/object serializers rather than JSON-encoding a bare ``MultiPicklistValue``: JSON encoders treat str subclasses as their original string value and do not call their overridden ``__str__`` method.