Skip to content

fix: JSON-escape values templated into a component's vega_spec - #10042

Merged
nishantmonu51 merged 1 commit into
mainfrom
nishant/fix-vega-spec-json-escaping
Oct 21, 2026
Merged

nishantmonu51 merged 1 commit into
mainfrom
nishant/fix-vega-spec-json-escaping

Conversation

@nishantmonu51

@nishantmonu51 nishantmonu51 commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

ResolveComponent resolved a custom chart's vega_spec with the plain template data, so a field display name or string param containing " or \ was spliced into the JSON unescaped. An ejected chart bound to a measure with display_name: 'Revenue "net"' then failed in InjectVegaParams with InvalidArgument, or in the renderer with "Invalid Vega-Lite spec" when the component had no scalar params.

  • Resolve vega_spec with a copy of the template data in which every string in .params, .fields, .env and .user is JSON-escaped (canvas.EscapeJSONStrings), so substituted values are valid inside the JSON strings they land in.
  • The spec is still resolved as one template, so template scope across JSON strings is unchanged, and scalar params are injected from the unescaped args as before.
  • Already-ejected specs are fixed without re-ejecting; no frontend change.
  • Known trade-off: a string value spliced into the spec as raw JSON rather than inside a JSON string, e.g. a string param holding {"a": "b"}, is now escaped too. Values without quotes or backslashes are unaffected.

Steps to reproduce:

  1. Enable customComponents in rill.yaml.
  2. Give a measure display_name: 'Revenue "net"'.
  3. Create a custom viz bound to it and eject it to Vega-Lite. The chart errors on every render.

Checklist:

  • Covered by tests
  • Ran it and it works as intended
  • Reviewed the diff before requesting a review
  • Checked for unhandled edge cases
  • Linked the issues it closes
  • Checked if the docs need to be updated. If so, create a separate Linear DOCS issue
  • Intend to cherry-pick into the release branch
  • I'm proud of this work!

Comment thread runtime/server/canvases.go Outdated
props := canvas.CoerceScalarParams(spec.RendererProperties.AsMap(), spec.Params, effectiveArgs)

// A custom chart's vega_spec is a JSON document held in a string, so it is set aside and
// resolved per JSON string below; resolving it as one string would splice values in unescaped.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a path here where we could escape the values already in TemplateData while calling ResolveTemplate? We could avoid unmarshal => marshal altogether.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 7991e9f: the vega_spec is now resolved as one template with a copy of the template data in which every string in .params, .fields, .env and .user is JSON-escaped (canvas.EscapeJSONStrings). No unmarshal or marshal, and template scope across JSON strings is unchanged. The one trade-off is that a string value spliced in as raw JSON (outside a JSON string) is escaped too; I noted it in the description.

Comment thread runtime/canvas/params.go Outdated

// UseNumber keeps numeric literals as written, so re-encoding doesn't alter their precision or notation.
dec := json.NewDecoder(strings.NewReader(vegaSpec))
dec.UseNumber()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this really help? InjectVegaParams doesnt do the same so precision is lost anyway no?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, it only helped the no-scalar-param case. Gone with the rewrite: the spec is no longer decoded here at all.

Comment thread runtime/canvas/params.go Outdated
return string(out), nil
}

func resolveJSONStrings(val any, resolve func(string) (string, error)) (any, error) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: reuse ResolveTemplateRecursively

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moot with the rewrite: there is no JSON walk any more, and runtime/canvas cannot import the parser anyway since runtime/parser imports runtime/canvas.

@nishantmonu51
nishantmonu51 force-pushed the nishant/fix-vega-spec-json-escaping branch from 73308af to 7991e9f Compare October 21, 2026 05:07
ResolveComponent resolved a custom chart's vega_spec with the plain template
data, so a field display name or string param containing a quote or backslash
was spliced into the JSON unescaped. An ejected chart bound to a measure with
display_name 'Revenue "net"' then failed in InjectVegaParams (or, without
scalar params, in the renderer as "Invalid Vega-Lite spec").

Resolve the vega_spec with a copy of the template data in which every string
(.params, .fields, .env, .user) is JSON-escaped, so substituted values are
valid inside the JSON strings they land in. The spec is still resolved as a
single template, so template scope across JSON strings is unchanged, and the
scalar params are injected from the unescaped args as before.
@nishantmonu51
nishantmonu51 force-pushed the nishant/fix-vega-spec-json-escaping branch from 7991e9f to 1df318b Compare October 21, 2026 05:09

@AdityaHegde AdityaHegde left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we now pre-escape some edge cases like

  1. {{ if eq .params.label "a \"b\"" }}, dont match anymore.
  2. {{ len .params.label }} will return escaped length.
    We could perhaps document this so users are not thrown off guard?

@nishantmonu51

Copy link
Copy Markdown
Collaborator Author

Sure, will add docs in #10029 PR.

@nishantmonu51
nishantmonu51 merged commit 781d6b7 into main Oct 21, 2026
13 checks passed
@nishantmonu51
nishantmonu51 deleted the nishant/fix-vega-spec-json-escaping branch October 21, 2026 06:49
nishantmonu51 added a commit that referenced this pull request Oct 21, 2026
Since #10042, a component's vega_spec is resolved with JSON-escaped template
data. Say so where vega_spec is documented: the Templating section of the
Vega-Lite page, the vega_spec schema description (and the generated component
reference), and the developer agent's Vega-Lite rules, which now tell it to
keep every placeholder inside a JSON string.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants