Closes: #386 - Exportable schema support#449
Conversation
|
@claude /review |
|
Claude finished @bctiemann's task in 1m 49s —— View job PR Review: Exportable Schema Support (#449)Tasks:
OverviewThis PR introduces the foundational data model and JSON Schema format for a portable, exportable Custom Object Type (COT) schema. It adds:
This is solid foundational work. The design decisions are well-reasoned and the code is clean. Below are observations ranging from correctness issues to minor suggestions. Issues1. Race condition in
|
|
Addressed items 1 and 3 in the summary, and also added a test to cover item 9 in Code Quality. The rest are non-issues for the time being. |
# Conflicts: # netbox_custom_objects/models.py
arthanson
left a comment
There was a problem hiding this comment.
- Model validators vs. identifier pattern mismatch
CustomObjectType.name and CustomObjectTypeField.name both validate to ^[a-z0-9_]+$ (no double underscores). The JSON Schema identifier pattern (cot_schema_v1.json:675) is ^[a-z0-9]+(_[a-z0-9]+)*$, which additionally disallows leading and trailing underscores.
So a COT or field named foo or bar is valid in the database but would produce an export that fails its own schema validator. The comment on identifier calls this out for field names, but cot_definition.name (schemas/cot_schema_v1.json:943) uses the same pattern — the gap applies to COT names too.
Seems like the validators should match.
- schema_id - I'm a bit confused on this. On one hand it is an auto-incrementing integer value and it is exported into the json schema. It looks like this is to avoid conflicts between different people making updates to the schema but I think that would break down if they make two completely independent changes at the same time, the generated schema_id would be the same.
If it is not to avoid conflicts between different people, then it seems we could just use the fields pk. If it is to avoid conflicts it seems a UUID would be much better / safer for this.
Design note:
|
|
@arthanson Design discussion has settled that we are going with a single-source-of-truth model, i.e. many NetBox instances pull down an authoritative COT schema from a central repository, rather than trying to merge many peer schema changes. So for the Note, this generally follows the pattern established by GRPC/protobuf, which also uses integers for its field IDs and places the onus on users to increment and curate them properly. |
|
@arthanson I addressed your concern about the regex validation on the The migrations will cause failures until we've merged #472, so that should go in next. |
Resolve migration number collision: - Feature's 0005_related_name was renumbered to 0006 (after 0005_context was inserted on feature); our 0006_portable_schema conflicted. - Resolution: accept feature's 0006_customobjecttypefield_related_name_and_more as authoritative 0006; rename our 0006_portable_schema → 0007_portable_schema and 0007_backfill_schema_ids → 0008_backfill_schema_ids; update dependencies and remove three duplicate operations from 0007 that are already in feature's 0006 (AddField related_name, AddConstraint unique_related_name; the AlterField for COTF.name is kept as a single-regex override).
arthanson
left a comment
There was a problem hiding this comment.
The basics look good - but can't really test much. One note to change before merge - the schema_id is not read-only in the serializer and it should be. It was stated it can't change, but right now someone could post/put to it and change it.
schema_id is system-assigned to ensure stable rename detection across
COT schema versions. Adding read_only_fields = ('schema_id',) to the
serializer Meta prevents POST/PATCH from overwriting it.
Adds SchemaIdReadOnlyTest to verify: schema_id appears in responses,
supplied values on POST are ignored, and PATCH attempts are silently
discarded.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Django's built-in test Client ignores format='json' (that's a DRF APIClient feature); PATCH with form-encoded body gets rejected with 415. Use json.dumps + content_type='application/json' instead. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…PATCH
attrs['type'] raises KeyError on any PATCH that omits the type field.
Use attrs.get('type') so the object/select-type checks are simply
skipped when type is not being updated, which is the correct behaviour
for a partial update.
This was exposed by the new SchemaIdReadOnlyTest.test_schema_id_ignored_on_patch
test, which sends a PATCH body containing only schema_id.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Closes: #386
Provides the groundwork and core data model for the portable COT schema format. Introduces stable field identifiers (
schema_id), field lifecycle metadata (deprecation / scheduled removal), theschema_documentstore, and the canonical JSON Schema definition (cot_schema_v1.json) against which schema documents are validated.Model changes —
CustomObjectTypeFieldschema_idPositiveSmallIntegerField(nullable)deprecatedBooleanFielddeprecated_sinceCharFieldscheduled_removalCharFieldModel changes —
CustomObjectTypeschema_documentJSONField(nullable)next_schema_idPositiveIntegerField(default 0)QuerySet.update()to avoid clearing the model cacheschema_idis protected by aUniqueConstraint(fields=["schema_id", "custom_object_type"], condition=Q(schema_id__isnull=False)).next_schema_iddesign noteschema_idauto-assignment inCustomObjectTypeField.save()incrementsnext_schema_idviaCustomObjectType.objects.filter(pk=...).update(next_schema_id=new_id)rather thancot.save(). This bypasses thepost_savesignal that clears the in-process model cache, preventing a race wherecontribute_to_class()targets a freshly regenerated model class rather than the oneget_model()already returned to the field-save caller.cot_schema_v1.jsonFormal JSON Schema (draft 2020-12) for the portable document format. Key design points:
schema_version: "1"and atypesarray.text_field,integer_field,object_field, …) usesunevaluatedProperties: falseto prevent extra keys from silently passing validation — mirroring the strictness already present onremoved_field.removed_fieldtombstone entries carryid,name,type, and optionalremoved_inversion.discriminatorhint (OpenAPI 3.x) is included for tooling; standard JSON Schema validators use theoneOfbranches.schema_format.pyShared constants consumed by the exporter, comparator, and executor:
SCHEMA_FORMAT_VERSION— current format version string ("1")CHOICES_TO_SCHEMA_TYPE/SCHEMA_TYPE_TO_CHOICES— bidirectional mapping betweenCustomFieldTypeChoicesvalues and schema type namesFIELD_DEFAULTS— canonical default values for all field attributes (omitted from schema documents when equal to default)FIELD_TYPE_ATTRS— which type-specific attributes are valid per field typeTests added
FieldDefaultsConsistencyTestCase— cross-checks every key inFIELD_DEFAULTSagainst the corresponding model field default; catches drift between the constants and the model.SchemaIdBackfillTestCase— validates the0005migration logic: sequential assignment, monotonic counter update, gap-after-deletion prevention.SchemaFormatTestCase—unevaluatedPropertiesrejects extra keys on all 9 field subtypes;removed_fieldacceptsremoved_in.Note:
schema_documentis added in this PR but leftnull; the exporter (#388, PR #450) populates it.