cde-tool is the companion CLI for @PersistentModel.
It is intentionally optional.
The package's core value does not depend on this tool:
- actor isolation via
@NSModelActor/@NSMainModelActor - macro-based model declarations via
@PersistentModel
You can adopt those directly without bringing cde-tool into the project at all.
The CLI exists as a second layer for teams that want stronger workflow support, especially for:
- CI/CD drift detection
- config-driven generation and validation
- faster migration from existing Core Data projects
Its job is not to replace your model declarations. Its job is to keep the three layers of the system aligned:
- the Core Data source model (
.xcdatamodeld/.xcdatamodel) - the Swift source you write with
@PersistentModel - the generated boilerplate that follows the package's rules
This guide explains:
- why the tool exists
- when you should use it
- how to choose between
generate,validate, andinspect - how
conformanceandexactvalidation differ - how fix suggestions and safe autofix fit into validation
- how to build and use the CLI in practice
@PersistentModel is a source-level representation of a Core Data model.
That source representation must stay aligned with the real Core Data schema. If it drifts, the macro may still expand, but the model you are expressing is no longer the one you think you are shipping.
cde-tool exists to make that alignment explicit.
Think of the tool as a schema companion:
generatehelps you create source that matches the modelvalidatechecks that existing source still matches the modelvalidate --fixcan apply a conservative subset of deterministic fixesinspectshows how the tool currently understands the model and rulesbootstrap-confighelps you create an editable project config from an existing modelinit-configgives you a default config template
The tool does not replace the macro system.
The macro system is responsible for expanding correct declarations into working code.
The tool is responsible for helping you:
- create those declarations
- keep them aligned over time
- detect drift before it turns into runtime or migration problems
That means the layering is deliberate:
- start with the macros if you only want the runtime/API improvements
- add
cde-toolwhen you want workflow automation or CI/CD enforcement
You should use cde-tool when:
- you want to generate
@PersistentModelsource from an existing Core Data model - your project has multiple entities and you want a repeatable model-to-source workflow
- you want CI to detect schema/source drift
- you want a stable config-driven setup for
persistentName, storage methods, inverse hints, and validation rules
You may not need it if:
- you are only experimenting with a very small project
- you are writing the model declarations by hand once and do not need drift checking
- you are not using generated source files at all
For existing Core Data projects, generate is also a strong adoption tool.
It can quickly turn a legacy .xcdatamodeld into a usable @PersistentModel starting point,
similar in spirit to Xcode's model code generation, but aligned with CoreDataEvolution's rules and
macro surface.
That lowers the cost of getting from "this looks interesting" to "this runs in my project".
The bigger the model surface becomes, the more useful cde-tool becomes.
cde-tool is intentionally source-model-only.
It accepts:
.xcdatamodeld.xcdatamodel
It does not accept:
.momd.mom
That restriction is deliberate. The CLI needs source-model information such as version selection and Xcode code generation settings. Compiled model artifacts do not carry enough information for the tooling workflow.
The normal workflow is:
- start from a Core Data source model
- create or refine a config
- generate
@PersistentModelsource - add your own methods and computed properties outside the tool-managed files
- validate drift as the model evolves
Typical first-time setup:
cde-tool bootstrap-config \
--model-path Models/AppModel.xcdatamodeld \
--output cde-tool.jsonThen:
- edit the generated config
- run
generate - add your hand-written extension files
- run
validate
If validate reports only deterministic annotation or literal drift, you can optionally apply safe fixes:
swift run --skip-build cde-tool validate --config cde-tool.json --fixOr preview those edits without writing files:
swift run --skip-build cde-tool validate --config cde-tool.json --fix --dry-runFor normal development, you can use:
swift run --skip-build cde-tool --helpIf you want a reusable local binary, use:
bash Scripts/build-cde-tool.shThis script:
- builds
cde-toolin release mode - injects version metadata when git metadata is available
- prints the final binary path
You can also copy the binary to a local tools directory:
bash Scripts/build-cde-tool.sh --copy-to ~/binIf a binary already exists there:
bash Scripts/build-cde-tool.sh --copy-to ~/bin --forceThree version entry points are available:
cde-tool --version
cde-tool -v
cde-tool versionUse them as follows:
--version- concise output, suitable for scripts
-v- detailed build metadata
version- same detailed metadata, explicit command form
This matters because cde-tool is intentionally coupled to the library and macro semantics.
When debugging a report or CI failure, always check the tool version first.
The local version behavior is intentionally simple:
- if you build from a git checkout, the tool can report:
- version
- tag
- commit
- describe
- dirty
- if you build from a source archive without git metadata, the tool falls back to development metadata
That means:
- local source builds are still usable
- release-quality provenance should come from the release pipeline, not from ad hoc local archives
The current release strategy is split into two layers.
For local builds, the tool keeps the fallback mechanism:
cde-tool --version- concise version string
cde-tool -v- version
- tag
- commit
- describe
- dirty
cde-tool version- same detailed metadata as
-v
- same detailed metadata as
This is enough for:
- local debugging
- issue reports
- CI logs in repository builds
For release artifacts, the intended path is GitHub Actions.
When building from a release tag, the workflow should:
- build the
cde-toolbinary - generate
version.json - generate checksums
- upload the release assets
That keeps the release artifacts fully traceable even when users download binaries or source archives outside a git checkout.
The CLI supports JSON config files so you do not need to repeat long argument lists.
Quick distinction:
init-config- skeleton-only
- does not read a model
bootstrap-config- model-driven
- requires a real Core Data source model
Create a default template:
cde-tool init-config --output cde-tool.jsonOr print it directly:
cde-tool init-config --stdoutWhen using config files:
generatereads thegeneratesectionvalidatereads thevalidatesectioninspect --configreads thegeneratesection
Relative paths are resolved from the config file's directory.
That is true for:
modelPathmomcBin- output/source directories
- header template paths
In practice, most manual edits fall into three buckets:
- rename the Swift-facing property while keeping the Core Data persistent name stable
- choose the Swift type and storage method for a field
- allow a specific validate-time optionality mismatch
- rename the Swift-facing relationship property while keeping the Core Data relationship name stable
The important thing to remember is that config rules are keyed by the persistent model name, not by the generated Swift name.
That means:
attributeRules.<Entity>.<persistentAttributeName>relationshipRules.<Entity>.<persistentRelationshipName>
If you use both generate and validate, keep these rule blocks aligned in both sections.
Use attributeRules when the Core Data model should keep one field name, but the generated Swift
property should use another.
Example:
{
"generate": {
"attributeRules": {
"Item": {
"name": {
"swiftName": "title"
}
}
}
}
}Here:
- Core Data persistent field name:
name - generated Swift property name:
title - generated macro annotation:
@Attribute(persistentName: "name") var title: String ...
The key name is the persistent field from the model. It is not the Swift property name.
Use relationshipRules the same way for relationships.
Example:
{
"generate": {
"relationshipRules": {
"Item": {
"primary_category": {
"swiftName": "category"
}
}
}
}
}Here:
- Core Data persistent relationship name:
primary_category - generated Swift property name:
category
The generated @Relationship(...) annotation still uses the persistent relationship name from the
model. The config only changes the Swift-facing property name.
Use attributeRules when a field should not stay as the default primitive mapping.
Common examples:
- enum-backed raw storage
Codablepayload storageValueTransformer-backed storage
{
"generate": {
"attributeRules": {
"Item": {
"status_raw": {
"swiftName": "status",
"swiftType": "ItemStatus",
"storageMethod": "raw"
}
}
}
}
}Use this when the model stores a primitive field, but the Swift API should expose a
RawRepresentable type such as an enum.
{
"generate": {
"attributeRules": {
"Item": {
"config_blob": {
"swiftName": "config",
"swiftType": "ItemConfig",
"storageMethod": "codable"
}
}
}
}
}Use this when the model stores an encoded payload but the Swift API should expose a Codable value type.
{
"generate": {
"attributeRules": {
"Item": {
"keywords_payload": {
"swiftName": "keywords",
"swiftType": "[String]",
"storageMethod": "transformed",
"transformerName": "NSSecureUnarchiveFromData"
}
}
}
}
}For transformed storage:
storageMethodmust be"transformed"- the Core Data field must be modeled as
Transformable transformerNameis requiredswiftTypeshould be the Swift-facing property type you want the generated source to use
Use ignoreOptionality when the Core Data model field stays optional, but you intentionally want
the Swift-facing declaration to remain non-optional.
This is a validate-time escape hatch for a specific field. It is useful when the model is more
permissive than the API you expose to the rest of the app.
Example:
{
"validate": {
"attributeRules": {
"Item": {
"title": {
"ignoreOptionality": true
}
}
}
}
}This rule is intentionally narrow:
- it applies at the field level
- it only relaxes optionality mismatch checking
- it does not ignore name, type, default value, storage method, transformer, or other drift
Use typeMappings when you want to change the default Swift type chosen for a Core Data primitive
kind across the config section.
Example:
{
"generate": {
"typeMappings": {
"Integer 64": {
"swiftType": "Int"
}
}
}
}This changes the default mapping for all Integer 64 fields in that section unless a more
specific per-field attributeRules override is present.
Use typeMappings for broad defaults, and attributeRules for one-off exceptions.
- Start with
bootstrap-config --style explicitif you want a manifest you can review and edit in one place. - Use
inspectbefore and after a config change when you want to confirm how the tool resolved a field or relationship. - Prefer editing one entity at a time, then run
generateorvalidateimmediately. - When a field uses
raw,codable,composition, ortransformed, setswiftTypeexplicitly so the tool does not have to infer it.
validate diagnostics can carry fix suggestions.
These suggestions are model-derived. They are intended to show:
- what source shape the tool expects
- whether the mismatch is deterministic enough to rewrite automatically
- which edits belong to the safe autofix set
Safe autofix currently targets only cases that the tool can rewrite without guessing, such as:
- inserting a missing
@Relationship(inverse: ..., deleteRule: ...) - correcting
inverseordeleteRuleinside an existing@Relationship - correcting
@Attribute(...)metadata such aspersistentName,.unique,.transient,storageMethod, transformer type, or decode failure policy - correcting a direct default-value literal when the model already defines the expected literal
Autofix intentionally does not rewrite higher-risk cases such as:
- broader renames that would require updates outside the property declaration
@Ignoreinference- storage-strategy migrations that need developer review
- complex default-value expressions that are not already represented as a direct literal
This keeps --fix conservative. The tool only rewrites what it can determine from the model and
current generation rules without introducing new assumptions.
generate turns a Core Data source model plus rules into @PersistentModel source files.
Example:
cde-tool generate \
--config cde-tool.jsonOr direct arguments:
cde-tool generate \
--model-path Models/AppModel.xcdatamodeld \
--output-dir Sources/AppModels \
--module-name AppModelsUse generate when:
- you are creating source for the first time
- the model changed and you want the source regenerated
- you want tool-managed files to follow current naming and storage rules
Useful flags:
--dry-run true- show planned writes without touching disk
--single-file true- emit one managed file
--split-by-entity true- emit one managed file per entity
--emit-extension-stubs true- create companion extension files for hand-written methods and computed properties
validate checks whether the current source still matches the model and the configured rules.
Example:
cde-tool validate --config cde-tool.jsonOr:
cde-tool validate \
--model-path Models/AppModel.xcdatamodeld \
--source-dir Sources/AppModels \
--module-name AppModelsThe tool supports two validation modes:
conformanceexact
conformance checks rules.
It validates whether the source written by the developer still conforms to:
- the Core Data model
- the configured naming and storage rules
- the package's
@PersistentModelconstraints
validate.attributeRules can also carry narrow field-level exceptions such as
ignoreOptionality: true for cases where the model remains optional but the Swift-facing property
is intentionally kept non-optional.
This mode does not require tool-managed files to be byte-for-byte identical to the current generator output.
Use conformance when:
- developers may make limited source-level adjustments
- you care about correctness more than exact generated text
- generated files may still pass through normal project tooling
exact checks unchanged generated output.
It first performs conformance, then additionally verifies that tool-managed files match the
current generator output exactly.
exact does not compile downstream generated targets for you.
It is a source/output consistency check, not a guarantee that another package target consuming the
generated files has been compiled successfully. If your workflow depends on generated models being
buildable as a separate target, keep an explicit swift build step in CI or local verification.
That means:
- managed file paths must match
- managed file contents must match
- stale managed files are reported
- hand-edited managed files are reported
Use exact when:
- you want CI to enforce a no-drift rule
- generated files are treated as read-only artifacts
- your team wants regeneration to be the only way managed files change
exact is intentionally strict.
Do not use it as the default mental model for every project.
If you adopt exact, you must also adopt its constraints:
- do not hand-edit tool-managed files
- do not run formatters or auto-fixers over tool-managed files
- do not let lint tools rewrite whitespace or imports in tool-managed files
If formatting changes the managed file text, exact will report drift even when semantics did not
change.
This is the recommended pattern:
- let
cde-toolown the managed file - put your custom methods in a separate extension file
- put your computed properties in a separate extension file
Example:
// Sources/AppModels/Item+Extensions.swift
extension Item {
var displayTitle: String {
title.uppercased()
}
func markAsRead() {
isRead = true
}
}This matters especially in exact mode.
If you add methods or computed properties directly into a tool-managed file, the next exact validation will report drift.
If you use:
--emit-extension-stubs truethe generator will create companion extension files to make this pattern obvious from the start.
inspect is a debugging command.
It loads the model and resolved rules, then prints the intermediate representation (IR) as JSON.
Example:
cde-tool inspect \
--model-path Models/AppModel.xcdatamodeldOr with config:
cde-tool inspect \
--model-path Models/AppModel.xcdatamodeld \
--config cde-tool.json--config can supply generation rules, but inspect still requires --model-path.
Use inspect when:
- you want to see how the tool currently resolves attribute names
- you want to check storage methods and inverse hints
- you are debugging config rules or generation behavior
This command is especially useful before changing config or when a generate/validate result looks surprising.
bootstrap-config creates an editable config scaffold from a real model.
By default it emits a compact scaffold. That keeps the first draft focused on the rules and placeholders you are most likely to edit first.
Example:
cde-tool bootstrap-config \
--model-path Models/AppModel.xcdatamodeld \
--output cde-tool.jsonIf you want a complete manifest that also writes the current default mappings explicitly, use:
cde-tool bootstrap-config \
--model-path Models/AppModel.xcdatamodeld \
--style explicit \
--output cde-tool.jsonUse it when:
- you are adopting
cde-toolin an existing project - you want a starting point for
typeMappingsandattributeRules - you want generate and validate to share one explicit rule set
Use --style explicit when:
- you want to review every attribute, relationship, and composition mapping in one file
- you want to hand-edit a full config instead of only the non-default parts
- you want a round-trippable manifest that makes the tool's current defaults visible
The generated config is meant to be edited.
It is not a final answer; it is a starting point.
init-config creates a default JSON template without reading a model.
Use it when:
- you want a clean config skeleton
- you already know the structure you want
- you do not need a model-driven scaffold
Examples:
cde-tool init-config --output cde-tool.json
cde-tool init-config --output cde-tool.json --preset minimal
cde-tool init-config --output cde-tool.json --force
cde-tool init-config --stdoutUseful options:
--preset minimal- emit a smaller starter template
--preset full- emit the full template; this is the default
--force- overwrite an existing config file when writing to disk
The tool does not:
- replace macro expansion
- validate macro-expanded implementation details directly
- accept compiled
.mom/.momdas the main workflow input - make generated files safe to hand-edit under
exact
It also does not remove the need to understand the model rules from PersistentModelGuide.md.
The CLI works because those rules are intentionally strict and tooling-friendly.
For a team project, the most stable approach is:
- keep the Core Data source model as the schema source of truth
- keep
cde-tool.jsonin the repository - generate source from the tool, not by hand-copying patterns
- put custom behavior in extension files
- use
conformancelocally - use
exactin CI only if your team is ready to treat managed files as read-only
That gives you:
- clear ownership of generated files
- explicit config-driven rules
- predictable review diffs
- earlier drift detection