Skip to content

Fix env in kernel.json serialized as array instead of JSON object (issue #1499) - #1666

Draft
alexarchambault with Copilot wants to merge 4 commits into
mainfrom
copilot/fix-issue-1499
Draft

Fix env in kernel.json serialized as array instead of JSON object (issue #1499)#1666
alexarchambault with Copilot wants to merge 4 commits into
mainfrom
copilot/fix-issue-1499

Conversation

Copilot AI commented Apr 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #1499.

Since v0.14.0-RC15, installing the Almond kernel would produce a broken kernel.json where the env field was a JSON array of pairs instead of a JSON object:

"env": [["COURSIER_REPOSITORIES", "repo-dummy"]]   ← broken (RC15+)
"env": {"COURSIER_REPOSITORIES": "repo-dummy"}     ← correct (RC14)

Jupyter rejects the broken format with:

TraitError: The 'env' trait of a KernelSpec instance expected a dict, not the list

Root Cause

The regression was introduced with the Scala 3 toolchain update between RC14 and RC15. In jsoniter-scala 2.13.5 under Scala 3, the macro does not recognise Map[String, String] as a JSON-object type and instead falls back to treating it as an Iterable[(String, String)], which is serialised as a JSON array of pairs.

Fix

Introduce a new ActualMap[T](map: Map[String, T]) wrapper class with a general implicit def codec[T: JsonValueCodec]: JsonValueCodec[ActualMap[T]] that always serialises and deserialises as a JSON object. The KernelSpec.env field is changed from Map[String, String] to ActualMap[String], allowing KernelSpec.codec to remain a simple JsonCodecMaker.make-derived codec — the macro picks up the ActualMap codec from its companion object, the same pattern used by RawJson elsewhere in the codebase.

  • Sorts map entries alphabetically for deterministic output.

A regression test is added to ProtocolTests that asserts env serialises as {"KEY":"VALUE"} and round-trips correctly.

…sue #1499)

Agent-Logs-Url: https://github.com/almond-sh/almond/sessions/439e350a-398b-4ff7-a53c-c97942efc16d

Co-authored-by: alexarchambault <7063723+alexarchambault@users.noreply.github.com>

@alexarchambault alexarchambault left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Fix the CI errors

Agent-Logs-Url: https://github.com/almond-sh/almond/sessions/e8895e66-f46c-403a-bf2d-810905544233

Co-authored-by: alexarchambault <7063723+alexarchambault@users.noreply.github.com>

Copilot AI commented Apr 22, 2026

Copy link
Copy Markdown
Contributor Author

Fix the CI errors

Fixed in 6f41165. The compilation errors were caused by a do-while loop, which was removed from Scala 3. Converted it to an equivalent while loop with a hasMore boolean flag.

@alexarchambault alexarchambault left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can't you instead introduce a custom case class ActualMap[T](map: Map[String, T]), define a codec for it, and use it in KernelSpec? So that we can keep using the jsoniter-scala macro when defining the KernelSpec codec

…codec to use macro

Agent-Logs-Url: https://github.com/almond-sh/almond/sessions/6fd1f7a6-94b9-42a6-94c6-af186ee0cd7d

Co-authored-by: alexarchambault <7063723+alexarchambault@users.noreply.github.com>

Copilot AI commented Apr 22, 2026

Copy link
Copy Markdown
Contributor Author

Can't you instead introduce a custom case class ActualMap[T](map: Map[String, T]), define a codec for it, and use it in KernelSpec? So that we can keep using the jsoniter-scala macro when defining the...

Done in 18f05dd. Introduced ActualMap[T](map: Map[String, T]) in ActualMap.scala with a custom implicit val stringCodec: JsonValueCodec[ActualMap[String]] that always serializes as a JSON object. KernelSpec.env is now typed as ActualMap[String] and KernelSpec.codec is back to JsonCodecMaker.make — the macro picks up ActualMap.stringCodec from the companion object, same pattern as RawJson.

final case class ActualMap[T](map: Map[String, T])

object ActualMap {
implicit val stringCodec: JsonValueCodec[ActualMap[String]] =

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can't you define a more general implicit def codec[T: JsonValueCodec]: JsonValueCodec[ActualMap[T]] = ... here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in cb3654e. Replaced implicit val stringCodec with implicit def codec[T: JsonValueCodec]: JsonValueCodec[ActualMap[T]], using tCodec.decodeValue/tCodec.encodeValue for the value type instead of the String-specific methods.

Agent-Logs-Url: https://github.com/almond-sh/almond/sessions/fec25bc7-ddd1-4672-b8ce-09a77d91dadc

Co-authored-by: alexarchambault <7063723+alexarchambault@users.noreply.github.com>
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.

The 'env' trait of a KernelSpec instance expected a dict, not the list

2 participants