Skip to content

Commit 11145f2

Browse files
committed
Add canonical contract pattern docs
1 parent bdb45d1 commit 11145f2

4 files changed

Lines changed: 186 additions & 68 deletions

File tree

README.md

Lines changed: 15 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,9 @@
1414
[![Last Commit](https://img.shields.io/github/last-commit/adz/CodecMapper)](https://github.com/adz/CodecMapper/commits/main)
1515
[![Stars](https://img.shields.io/github/stars/adz/CodecMapper?style=social)](https://github.com/adz/CodecMapper/stargazers)
1616

17-
`CodecMapper` is a schema-first serialization library for F# focused on explicit wire contracts, symmetric encode/decode behavior, and execution that stays friendly to Native AOT and Fable-style targets.
17+
`CodecMapper` is a schema-first serialization library for F# focused on explicit wire contracts, symmetric encode/decode behavior, and portability to Native AOT and Fable-style targets.
1818

19-
It is for the cases where serializer attributes and implicit conventions stop being helpful: you want the wire shape to be visible in code, you want encode and decode to stay in sync, and you want the contract to read like the data it describes.
20-
21-
The core idea is simple: define one schema that mirrors your record shape, then compile it into reusable codecs.
22-
23-
That sits between two common extremes:
24-
25-
- Put serializer attributes directly on domain types and let wire concerns leak into the model.
26-
- Introduce separate DTOs and mapping layers so the wire contract stays explicit, but now maintain extra types and conversion code.
27-
28-
`CodecMapper` keeps the useful part of the DTO approach, explicit contracts, without forcing a duplicate object model for every message shape. The schema is the contract, and it sits next to the data instead of behind a second translation layer.
19+
It is for cases where serializer attributes and implicit conventions stop being helpful. You define one schema that mirrors the wire shape, then compile it into reusable codecs.
2920

3021
## Why the schema feels different
3122

@@ -84,16 +75,13 @@ The result is not hidden serializer behavior. It is the contract itself, written
8475

8576
If you want the compile step to read a bit smaller in samples, the format modules also expose `Json.codec`, `Xml.codec`, `Yaml.codec`, and `KeyValue.codec` as direct aliases for `compile`.
8677

87-
## Why this is useful
78+
## Why use it
8879

8980
- The schema mirrors the data, so changes to the wire contract are visible in one place.
9081
- Encode and decode come from the same definition, so drift is harder to introduce accidentally.
9182
- `Json.compile` / `Json.codec` and `Xml.compile` / `Xml.codec` reuse the same schema instead of making you maintain separate mappings.
9283
- Domain refinement stays explicit through `Schema.map` and `Schema.tryMap` instead of being buried in serializer settings.
93-
- Versioned message and config contracts stay deliberate because the wire shape is authored directly instead of inferred from whatever the current model happens to look like.
94-
- Migration is easier to stage: keep the external contract stable, refine the in-memory domain behind `map` / `tryMap`, and only introduce DTOs when you genuinely need a separate transport model.
95-
96-
See [When models evolve](#when-models-evolve) for the concrete version of this tradeoff.
84+
- Versioned message and config contracts stay deliberate because the wire shape is authored directly.
9785

9886
## Why not just use X?
9987

@@ -109,35 +97,15 @@ See [When models evolve](#when-models-evolve) for the concrete version of this t
10997
| JSON Schema-first | Varies | Varies | External schema owned | Integrating with schema-owned systems |
11098
| `CodecMapper` | Strong | Strong | Authored schema contract | Explicit message/config contracts across JSON/XML |
11199

112-
Other dimensions that matter:
113-
114-
- Format symmetry: `CodecMapper` is centered on one contract driving both JSON and XML, while most alternatives are JSON-only or serializer-specific.
115-
- Model evolution: `CodecMapper` keeps contract edits explicit and supports domain refinement with `Schema.map` / `Schema.tryMap` before you reach for DTO duplication. See [When models evolve](#when-models-evolve).
116-
- Tooling relationship: `CodecMapper` can export JSON Schema from the authored contract and also import external JSON Schema when you are adapting to another system.
117-
118-
If your question is "why not just use `System.Text.Json`?", the short answer is: use it when convention-based object serialization is enough. Use `CodecMapper` when you want the contract itself to be visible, reviewable, reusable, and stable across model evolution.
100+
Use `System.Text.Json` when convention-based object serialization is enough. Use `CodecMapper` when you want the contract itself to be visible, reviewable, reusable, and stable across model evolution.
119101

120102
## Where it fits well
121103

122104
`CodecMapper` is strongest when the wire contract matters and you want it to stay explicit.
123105

124-
For message contracts:
125-
126-
- Define the exact payload shape once.
127-
- Compile it into reusable codecs.
128-
- Keep version changes visible in the schema instead of relying on serializer conventions.
129-
130-
For configuration contracts:
131-
132-
- Treat config as a real contract rather than as incidental object serialization.
133-
- Keep migration and versioning logic deliberate.
134-
- Use the schema as the stable boundary even if the in-memory domain gets richer over time.
135-
136-
For domain models:
137-
138-
- Keep the domain type close to the wire contract when that is useful.
139-
- Use `Schema.map` and `Schema.tryMap` when the runtime model should be stronger than the serialized shape.
140-
- Introduce separate DTOs only when the transport model genuinely needs to diverge.
106+
- Message contracts: define the payload shape once and keep changes visible in the schema.
107+
- Config contracts: treat configuration as a versioned boundary instead of incidental object serialization.
108+
- Domain refinement: use `Schema.map` and `Schema.tryMap` when the runtime model should be stronger than the serialized shape.
141109

142110
## How JSON Schema fits in
143111

@@ -252,31 +220,14 @@ Compared with DTO-heavy designs, the difference is:
252220
- The shared sentinel now includes selected invalid and out-of-range numeric cases, so the portability story covers failure behavior as well as happy-path round-trips.
253221
- The contract bridge in [src/CodecMapper.Bridge](/home/adam/projects/CodecMapper/src/CodecMapper.Bridge) is `.NET`-only by design; the portable surface is the core schema/JSON/XML library in [src/CodecMapper](/home/adam/projects/CodecMapper/src/CodecMapper).
254222

255-
## Start here
256-
257-
- Read [Getting started](docs/GETTING_STARTED.md) for the core mental model and schema DSL.
258-
259-
## More docs
260-
261-
Tutorials:
262-
263-
- [Getting started](docs/GETTING_STARTED.md)
264-
265-
How-to guides:
266-
267-
- [How to export JSON Schema](docs/HOW_TO_EXPORT_JSON_SCHEMA.md)
268-
- [How to import existing C# contracts](docs/HOW_TO_IMPORT_CSHARP_CONTRACTS.md)
269-
- [Configuration contracts guide](docs/CONFIG_CONTRACTS.md)
270-
271-
Reference:
272-
273-
- [JSON Schema support reference](docs/JSON_SCHEMA_SUPPORT.md)
274-
- [API docs](https://adz.github.io/CodecMapper/)
275-
276-
Explanations:
223+
## Docs
277224

278-
- [JSON Schema in CodecMapper](docs/JSON_SCHEMA_EXPLANATION.md)
279-
- [C# attribute bridge design](docs/CSHARP_ATTRIBUTE_BRIDGE.md)
225+
- Start with [Getting started](docs/GETTING_STARTED.md).
226+
- Copy from [How to model common contract patterns](docs/HOW_TO_MODEL_COMMON_CONTRACT_PATTERNS.md).
227+
- Use [Configuration contracts guide](docs/CONFIG_CONTRACTS.md) for versioned config shapes.
228+
- Use [How to export JSON Schema](docs/HOW_TO_EXPORT_JSON_SCHEMA.md) and [JSON Schema support reference](docs/JSON_SCHEMA_SUPPORT.md) for schema interchange.
229+
- Use [How to import existing C# contracts](docs/HOW_TO_IMPORT_CSHARP_CONTRACTS.md) and [C# attribute bridge design](docs/CSHARP_ATTRIBUTE_BRIDGE.md) for the bridge/facade story.
230+
- Browse the [API docs](https://adz.github.io/CodecMapper/).
280231

281232
## Benchmarks
282233

TASKS.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,7 @@ Completed rename, parser, bridge, compatibility, JSON Schema, docs, and projecti
2525

2626
- [x] **Task 32:** Added path-aware decode diagnostics across `Json`, `Xml`, `KeyValue`, and `Yaml`, including missing-field paths, collection indices/items, and `Schema.tryMap` validation context, with matching regression coverage in the unit test suite.
2727

28-
- [ ] **Task 33: Ship canonical pattern docs**
29-
- Add copy-pasteable reference patterns for basic records, nested records, validated wrappers, versioned contracts, config contracts, JSON Schema import, and the C# bridge.
30-
- Keep the examples aligned with the stable `Schema.define |> Schema.construct |> ... |> Schema.build` DSL.
31-
- Make the “small explicit DSL” and compile-once workflow easy to discover from README and docs landing pages.
28+
- [x] **Task 33:** Added a canonical contract-pattern guide covering basic records, nested records, validated wrappers, versioned contracts, config contracts, JSON Schema import, and the C# bridge, and linked it from the README and docs landing pages so the copy-paste patterns are easy to find.
3229

3330
- [ ] **Task 34: Keep Task 18 focused on build-time code generation**
3431
- Generate ordinary checked-in F# schema code rather than introducing a second runtime schema system.
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
# How To Model Common Contract Patterns
2+
3+
Use this guide when you already know the kind of contract you want to model and need a copy-pasteable starting point.
4+
5+
All examples stay on the stable authored DSL:
6+
7+
```fsharp
8+
Schema.define<'T>
9+
|> Schema.construct ctor
10+
|> Schema.field ...
11+
|> Schema.build
12+
```
13+
14+
## Basic record
15+
16+
```fsharp
17+
open CodecMapper
18+
19+
type Person = { Id: int; Name: string }
20+
let makePerson id name = { Id = id; Name = name }
21+
22+
let personSchema =
23+
Schema.define<Person>
24+
|> Schema.construct makePerson
25+
|> Schema.field "id" _.Id
26+
|> Schema.field "name" _.Name
27+
|> Schema.build
28+
29+
let codec = Json.codec personSchema
30+
```
31+
32+
## Nested record
33+
34+
```fsharp
35+
type Address = { Street: string; City: string }
36+
let makeAddress street city = { Street = street; City = city }
37+
38+
type Person = { Id: int; Name: string; Home: Address }
39+
let makePerson id name home = { Id = id; Name = name; Home = home }
40+
41+
let addressSchema =
42+
Schema.define<Address>
43+
|> Schema.construct makeAddress
44+
|> Schema.field "street" _.Street
45+
|> Schema.field "city" _.City
46+
|> Schema.build
47+
48+
let personSchema =
49+
Schema.define<Person>
50+
|> Schema.construct makePerson
51+
|> Schema.field "id" _.Id
52+
|> Schema.field "name" _.Name
53+
|> Schema.fieldWith "home" _.Home addressSchema
54+
|> Schema.build
55+
```
56+
57+
Use `Schema.fieldWith` when the child value has its own explicit schema boundary.
58+
59+
## Validated wrapper
60+
61+
```fsharp
62+
type UserId = UserId of int
63+
64+
module UserId =
65+
let create value =
66+
if value > 0 then Ok(UserId value)
67+
else Error "UserId must be positive"
68+
69+
let value (UserId value) = value
70+
71+
type Account = { Id: UserId; Name: string }
72+
let makeAccount id name = { Id = id; Name = name }
73+
74+
let userIdSchema =
75+
Schema.int
76+
|> Schema.tryMap UserId.create UserId.value
77+
78+
let accountSchema =
79+
Schema.define<Account>
80+
|> Schema.construct makeAccount
81+
|> Schema.fieldWith "id" _.Id userIdSchema
82+
|> Schema.field "name" _.Name
83+
|> Schema.build
84+
```
85+
86+
If a wrapper rule repeats across multiple contracts, extract that `Schema.tryMap` pipeline into a named schema value and reuse it explicitly.
87+
88+
## Versioned contract
89+
90+
For config files or messages that evolve over time, use an explicit envelope:
91+
92+
```fsharp
93+
type SettingsV2 = {
94+
Version: int
95+
Mode: string
96+
Region: string option
97+
}
98+
99+
let makeSettingsV2 version mode region =
100+
{ Version = version; Mode = mode; Region = region }
101+
102+
let settingsV2Schema =
103+
Schema.define<SettingsV2>
104+
|> Schema.construct makeSettingsV2
105+
|> Schema.fieldWith "version" _.Version (
106+
Schema.int
107+
|> Schema.tryMap
108+
(fun value ->
109+
if value > 0 then Ok value
110+
else Error "version must be positive")
111+
id
112+
)
113+
|> Schema.field "mode" _.Mode
114+
|> Schema.field "region" _.Region
115+
|> Schema.build
116+
```
117+
118+
If you need defaults and omission policies, compose the field-policy helpers directly at the field boundary.
119+
120+
## Config contract
121+
122+
Compile the same authored schema into config-oriented projections when the wire surface is flat or YAML-shaped:
123+
124+
```fsharp
125+
let keyValueCodec = KeyValue.codec settingsV2Schema
126+
let yamlCodec = Yaml.codec settingsV2Schema
127+
```
128+
129+
For a larger walkthrough of versioned configuration patterns, see [Config Contracts Guide](CONFIG_CONTRACTS.md).
130+
131+
## External JSON Schema import
132+
133+
Use the importer when another system owns the contract and you need a deterministic receive-side shape:
134+
135+
```fsharp
136+
let report =
137+
JsonSchema.importWithReport """
138+
{
139+
"type": "object",
140+
"properties": {
141+
"kind": { "type": "string" },
142+
"count": { "type": "integer" }
143+
},
144+
"required": [ "kind", "count" ]
145+
}
146+
"""
147+
148+
let codec = Json.codec report.Schema
149+
```
150+
151+
Imported schemas target `Schema<JsonValue>`. Keep authored `Schema<'T>` values as the source of truth when you control the contract.
152+
153+
## C# contract bridge
154+
155+
If you already have existing C# contracts, import them through the bridge or author a new explicit schema through the C# facade:
156+
157+
```csharp
158+
var userSchema =
159+
CSharpSchema.Record(() => new User())
160+
.Field("id", x => x.Id, (x, field) => x.Id = field)
161+
.Field("display_name", x => x.DisplayName, (x, field) => x.DisplayName = field)
162+
.Build();
163+
164+
var jsonCodec = CSharpSchema.Json(userSchema);
165+
var yamlCodec = CSharpSchema.Yaml(userSchema);
166+
```
167+
168+
For bridge import details and tradeoffs, see [How To Import Existing C# Contracts](HOW_TO_IMPORT_CSHARP_CONTRACTS.md) and [C# Attribute Bridge Design](CSHARP_ATTRIBUTE_BRIDGE.md).

docs/index.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ Use how-to guides when you already know what you want to accomplish.
1919
Generate JSON Schema documents or import external JSON Schema contracts.
2020
- [How To Import Existing C# Contracts](HOW_TO_IMPORT_CSHARP_CONTRACTS.md)
2121
Bring `System.Text.Json`, `Newtonsoft.Json`, or `DataContract` models into `CodecMapper`.
22+
- [How To Model Common Contract Patterns](HOW_TO_MODEL_COMMON_CONTRACT_PATTERNS.md)
23+
Start from copy-pasteable patterns for basic records, validated wrappers, config contracts, and bridge scenarios.
2224
- [Config Contracts Guide](CONFIG_CONTRACTS.md)
2325
Treat application configuration as an explicit versioned wire contract.
2426

0 commit comments

Comments
 (0)