Skip to content

fix(http-server-js): keep JSON escapes in generated string literals - #12135

Merged
Timothee Guerin (timotheeguerin) merged 1 commit into
microsoft:mainfrom
bimsonz:http-server-js-string-literal-escapes
Oct 8, 2026
Merged

Timothee Guerin (timotheeguerin) merged 1 commit into
microsoft:mainfrom
bimsonz:http-server-js-string-literal-escapes

Conversation

@bimsonz

Copy link
Copy Markdown
Contributor

Split out of #11980, where it came up in review.

escapeUnsafeChars is applied to JSON.stringify output, but its regex also matched \, so it doubled the escapes JSON.stringify had already written:

value emitted evaluates to
a"b "a\\"b" does not parse
c\d "c\\\\d" c\\d
x, newline, y "x\\ny" x\ny, a backslash and an n

The first crashes the emitter: model Cat { kind: "a\"b" } fails with Emitter "@typespec/http-server-js" crashed! because prettier cannot parse kind: "a\\"b";.

This removes \ from the regex and the map. The map comes from the example in CodeQL's js/bad-code-sanitization help, which has the same \ entry but a regex that never matches it. The control-character entries never match JSON.stringify output and are left as they are.

Testing

  • Two tests in scalar.test.ts: escapeUnsafeChars(JSON.stringify(v)) parses back to v, and string literal types containing a quote, a backslash or a newline are emitted as the same strings. Both fail on main.
  • http-server-js tests pass, and test:e2e gives the same result as on main.

escapeUnsafeChars is applied to JSON.stringify output, but it also escaped
backslashes, so it doubled the escapes JSON.stringify had written. A string
literal type containing a quote generated code that does not parse, and the
emitter crashed formatting it; one containing a backslash or a newline
generated a different string. Leave backslashes alone. The CodeQL
js/bad-code-sanitization example this map comes from has the same backslash
entry, but its regex never matches it.
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).
1 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

The focused fix preserves JSON semantics and includes meaningful regression coverage and a valid changelog entry.

0 open findings

What changed in this PR

Fixes double-escaped JSON string literals generated by the JavaScript HTTP server emitter.

Changes:

  • Preserves escapes produced by JSON.stringify.
  • Adds unit and emitter-level regression coverage.
  • Adds the required bug-fix changelog entry.
File Description
packages/​http-server-js/​src/​common/​reference.ts Stops re-escaping JSON backslashes.
packages/​http-server-js/​test/​scalar.test.ts Tests escaped literals and generated output.
.chronus/​changes/​http-server-js-string-literal-escapes-2026-9-7-23-15-0.md Records the user-visible fix.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

@pkg-pr-new

pkg-pr-new Bot commented Oct 8, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@typespec/http-server-js@12135

commit: 42d0462

@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

All changed packages have been documented.

  • ✅ @typespec/http-server-js
Show changes

@typespec/http-server-js - fix ✏️

Fix generated code for string literals containing a quote, a backslash or a newline.

Merged via the queue into microsoft:main with commit 25f3465 Oct 8, 2026
28 of 29 checks passed
Zach Bimson (bimsonz) added a commit to bimsonz/typespec that referenced this pull request Oct 8, 2026
Fixes the enum half of microsoft#2954. Union variants are not included, per the
triage comment.

`@encodedName` on an enum member was accepted but ignored. It now sets
the value the member is serialized as. Without it, a member is
serialized as its explicit value, or as its name when it has none.

```tsp
enum ConversationStatus {
  // Keeps 0 for protobuf, serialized as "unknown" in JSON
  @Encodedname("application/json", "unknown")
  CONVERSATION_STATUS_UNSPECIFIED: 0,

  @Encodedname("application/json", "ready")
  CONVERSATION_STATUS_READY: 1,
}
```

A dual protobuf and JSON surface needs this: protobuf requires integer
values and JSON wants strings, and overriding the name alone does not
help because the value always wins. Only `application/json` is resolved,
as OpenAPI cannot express a different enum value per media type.

### Changes

- compiler: `resolveEncodedEnumMemberValue(program, member, mimeType)`
returns the encoded name if one is declared, else the member value, else
the name. A JSON-based mime type such as `application/merge-patch+json`
falls back to the `application/json` name. Example, default and
discriminator values use it. `getDiscriminatedUnionFromInheritance` gets
a `(program, type, discriminator)` overload and `(type, discriminator)`
is deprecated, as `getDiscriminatedUnion` was in microsoft#6059. Since an encoded
name is now a member's value, the conflict check compares it with the
other members' values rather than their names.
- openapi3, json-schema: enum schemas, member references and
discriminator mappings use the resolved value; an encoded integer member
is emitted as a string. openapi3 validates an enum server variable on
the resolved values, so an integer enum whose members are all encoded
can be used as one.
- http-server-js: enum values, member literal types and union variant
differentiation.
- http-server-csharp: `JsonStringEnumMemberName` carries the encoded
name, and a non-integer enum whose members are all encoded is typed as
the enum instead of `double`.
- emitter-framework: TypeScript and Python enum declarations, union and
value expressions and atoms use the resolved value, so they match the
discriminator keys from the typekit. A Python enum with an encoded
member gives every member its value instead of `auto()`, which fails at
import beside a string on Python 3.13 and later; other Python enums are
unchanged. Rendering `EnumMember`, `ValueExpression` or `Atom` for an
enum value now needs a `TspContext`, so this changeset is `breaking` (a
minor bump at 0.x).

Fixes on `main` that came up in review are in microsoft#12135 (http-server-js
string literals) and in two http-server-csharp PRs to follow (string
escaping, and one `JsonPropertyName` per property). Until those land, an
encoded name containing a quote or a backslash generates code that does
not parse, does not compile or holds a different value, in an
http-server-js member literal type and in C#, as a string value
containing one does on `main`.

### Compatibility

The compiler changeset is `fix`: microsoft#2954 is triaged as a bug, and the
decorator was accepted and ignored, so this falls under the bug
exception in the breaking change policy. It does change output for any
spec that already has `@encodedName` on an enum member, and such a spec
can now report `invalid-discriminated-union-variant`,
`invalid-discriminator-value` or `encoded-name-conflict`. Encoding only
some members of an integer enum gives a mixed-type enum, which OpenAPI
3.0 rejects with `enum-unique-type`.

TCGC computes enum values as `value ?? name`, so clients generated
through it keep the member value until it is updated. No project in
azure-rest-api-specs uses `@encodedName` on an enum member.

### Testing

- compiler, openapi3, json-schema, emitter-framework, http-server-js and
http-server-csharp tests pass, and the website build's regenerated docs
are committed.
- The C# output for integer, float and string enums with encoded members
compiles under .NET 10 and round-trips through the generated
`JsonSerializationProvider`, and the Python output imports under Python
3.13 and 3.14.
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.

3 participants