fix(blueprints): render a YAML export as the document JSON renders - #388
Merged
Merged
Conversation
`pro blueprints export -o yaml` wrote each component's `configuration` as a sequence of the integer bytes of its own JSON text and lower-cased every key (`activationpredicate`), because yaml.v3 reads neither `json` tags nor encoding/json's treatment of `json.RawMessage`. `-o json` was correct all along. `apply --scaffold -o yaml` took the same two through printScaffold. The rendering and the read side had to move together. The byte sequence was the only spelling of `configuration` yaml.v3 could bind back, so fixing the export alone would have traded a broken-looking document for one that no longer applies — and the `json`-tag keys an export writes now bind to nothing in yaml.v3's own struct binding, which matches the lower-cased Go field name. - `jsonShaped` rebuilds a value from its own JSON encoding; printScaffold renders that, and `blueprintExport` returns it from `MarshalYAML`. The hook sits on the type rather than in printExport, which is shared with the Jamf Protect and Jamf School exports — their input types carry no `json` tags, so normalising there would rewrite their YAML into the Go field names their own apply path does not read back. - `unmarshalInput`'s YAML rung is normalised through JSON, so a document written either way binds (encoding/json matches a key case-insensitively) and a mapping reaches a json.RawMessage field. yaml.v3's binding stays as the last rung for the values encoding/json refuses and for an input carrying no content. - `refuseNonObjectComponentConfiguration` refuses the one document that rung now binds and must not send: a YAML export from v1.31.0 or earlier, whose byte sequence is a valid JSON array and would travel to the gateway as the component's configuration. Verified on the sandbox tenant: the YAML export of `JNUC Lab - Math Settings` carries `configuration` as a mapping with the `Calculator` and `SystemBehavior` keys, the JSON export is byte-identical to before, and that YAML file applies back (dry-run) with its scope resolved. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
grahampugh
approved these changes
Sep 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
pro blueprints export -o yamlwrote each component'sconfigurationas a sequence of the integer bytes of its own JSON text:123is{,34is".-o jsonwas correct all along and returned the real object.Two causes, both yaml.v3: it has no equivalent of
encoding/json's special case forjson.RawMessage, so it renders the field as the[]byteit actually is; and it does not readjsontags, so it derives keys by lower-casing the Go field name — henceactivationpredicatewhere JSON emitsactivationPredicate.apply --scaffold -o yamltook the same two throughprintScaffold, along with the--scaffold -o yamlofpro compliance-benchmarks applyandpro platform-device-groups create.Why the read side had to move too
The round trip worked before this change, by accident. yaml.v3 can bind a sequence of integers back into a
[]byte, so the corrupt byte sequence was the only spelling ofconfigurationits own reader accepted. Fixing the export alone trades a broken-looking document for one that no longer applies — verified in a throwaway probe: the old YAML applied fine, JSON-shaped YAML failed withparsing portable format: input is not valid JSON or YAML, and the lower-cased keys would have droppedactivationPredicatesilently regardless.So this is one fix, not two:
jsonShapedrebuilds a value from its own JSON encoding.printScaffoldrenders that, andblueprintExportreturns it fromMarshalYAML.printExport, which is shared with 20+ Jamf Protect and Jamf School export sites. Their SDK input types carry nojsontags at all (jamfprotect.AnalyticInputis untagged, so its JSON keys are alreadyLongDescription), so normalising centrally would have rewritten their YAML into Go field names their own apply path does not read back.unmarshalInput's YAML rung now normalises through JSON, so a document written either way binds —encoding/jsonmatches keys case-insensitively, making this a strict superset — and a mapping reaches ajson.RawMessagefield. yaml.v3's own binding stays as the last rung for valuesencoding/jsonrefuses and for input carrying no content.refuseNonObjectComponentConfigurationrefuses the one document that rung now binds and must not send: a YAML export from 1.31.0 or earlier, whose byte sequence is a valid JSON array and would otherwise travel to the gateway as the component's configuration.Verification
make build,go test ./...(fully green),make lint(0 issues),make fmtclean.Mutation-checked both halves: disabling
MarshalYAMLfailsTestBlueprintExportYAMLCarriesTheSameDocumentAsJSONandTestBlueprintExportYAMLAppliesBack; disabling the normalised read rung failsTestBlueprintExportYAMLAppliesBackandTestUnmarshalInput_YAMLBindsAJSONTagKeyAndARawMessage.TestEveryScaffoldYAMLBindsBackThroughUnmarshalInputcovers all three scaffold types through scaffold-YAML →unmarshalInput→ JSON with no field dropped.Live against a sandbox tenant:
The stale file was produced by the actual 1.31.0 release binary, not hand-written. Export is read-only and every write above went through
-n; the tenant still holds exactly its two pre-existing blueprints.Notes for review
mainand against the in-flight--page-sizebranch (Computer-Inventory--alldoesn't respect--page-size#385),CHANGELOG.mdincluded — trial-merged all three together with a green build, suite and lint.export/--scaffold -o yamloutput: their YAML keys are lower-cased Go field names rather than the JSON ones, so those documents are also not the same document as-o json. Self-consistent today, and after this change their read path binds either spelling. Making it uniform means giving those SDK-typed exports a canonical key spelling — a larger, separate change.🤖 Generated with Claude Code