# BSON Mapping Guide Design §10 and decision D-06. The representation of a value in BSON is a data contract, not an implementation detail: once a collection holds a million documents, changing how a `BigDecimal` is stored is a migration with downtime, not a code change. `MongoTypeRepresentationManifest` pins the representation so a library upgrade or a different default cannot move it. ## 1. The manifest `MongoTypeRepresentationManifest.standard()` fixes: | Java type | BSON | Representation type | |---|---|---| | `UUID` | `Binary` subtype 4 | `MongoUuidRepresentation.STANDARD` | | `BigDecimal` | `Decimal128` | `MongoDecimalRepresentation.DECIMAL_128` | | `BigInteger` | `Decimal128` (or `String` when out of range, declared) | `MongoBigIntegerRepresentation` | | `Instant` / `OffsetDateTime` / `ZonedDateTime` | UTC `Date` | `MongoTemporalRepresentation.UTC_DATE` | | `LocalDate` | `String` (ISO-8601) or UTC `Date`, declared per field | `MongoTemporalRepresentation` | | `enum` | `String` name | `MongoEnumRepresentation.NAME` | `MongoMappingConfiguration` and `MongoCustomConversionsFactory` build the Spring Data converters from the manifest, so there is one place to read and one place to change. ## 2. UUID `UuidRepresentation.STANDARD` (subtype 4), always. The driver's legacy Java representation (subtype 3) byte-swaps two halves of the UUID, so a document written by one representation and read by the other yields a different — and valid-looking — UUID. Nothing errors; you just get the wrong row. The golden snapshot kit pins the codec explicitly for this reason (`MongoBsonSnapshot.defaultRegistry()`). ## 3. Decimal `BigDecimal` → `Decimal128`, never `Double`. `12.30` stored as a double is `12.299999999999999`, and a monetary comparison written against it will one day be wrong by a cent for a customer who notices. `BigDecimalToDecimal128Converter` / `Decimal128ToBigDecimalConverter` are registered from the manifest. `Decimal128` has 34 significant digits; a `BigDecimal` beyond that range fails on write rather than rounding silently. ## 4. Time Store instants, not local times. `LocalDateTimeMappingGuard` refuses `LocalDateTime` fields on a mapped document: a `LocalDateTime` has no offset, so the value that goes in depends on the JVM default zone of whichever instance wrote it, and the two instances in a rolling deploy can disagree. Use `Instant` when the moment matters and `LocalDate` when the calendar day matters. ## 5. Type metadata `MongoTypeMetadataPolicy` decides what goes in `_class`: | Policy | Stored | Use when | |---|---|---| | `NONE` | nothing | The collection holds exactly one type and never will hold a subtype. | | `ALIAS` | a registered short alias | A polymorphic hierarchy in a long-lived collection. | | `CLASS_NAME` | the FQCN | Short-lived or internal collections only. | `PolicyAwareMongoTypeMapper` enforces it, and `MongoTypeMetadataRegistry` holds alias → class. A `@LongLivedMongoDocument` type with `CLASS_NAME` is refused: writing `com.example.OrderV2` into a million documents means that renaming the package is a data migration. ## 6. Missing versus null The golden kit keeps these apart deliberately. `MongoBsonSnapshotAssert.hasNoField(...)` and `hasExplicitNull(...)` are different assertions, because in MongoDB they are different documents: `{"a": null}` matches `{a: null}` and `{a: {$exists: true}}`, while `{}` matches only the first. A mapper change that starts writing explicit nulls silently changes what your queries return. ## 7. Golden representation tests Every collection with a fixed representation should have a snapshot test: ```java MongoBsonSnapshot snapshot = MongoBsonSnapshot.of(storedDocument); MongoBsonSnapshotAssert.assertThat(snapshot) .hasBsonType("amount", "DECIMAL128") .hasBsonType("externalId", "BINARY") .hasNoJavaClassName("dev.caskeleton") .hasTypeSignature("_id:OBJECT_ID,amount:DECIMAL128,createdAt:DATE_TIME,externalId:BINARY"); ``` `hasTypeSignature` is the regression gate: it fails on *any* representation change, including ones a value-equality assertion would pass. When it fails, the question is whether the change was intended and has a migration — not whether to update the string. ## 8. Round trips `MongoRoundTripContract` asserts that `write → read` returns an equal domain object *and* that `write → read → write` produces an identical BSON document. The second half is what catches an asymmetric converter: a value that reads back equal but re-serialises differently makes every subsequent `save()` a spurious update, and turns change streams into a noise generator.