From 2d3f49e4b265c6d074b0d3316806a454fccabeb8 Mon Sep 17 00:00:00 2001 From: Sandro Meier Date: Fri, 10 Apr 2026 17:28:54 +0200 Subject: [PATCH] dev_container: Handle devcontainer.metadata label as JSON object or array (#53557) Self-Review Checklist: - [x] I've reviewed my own diff for quality, security, and reliability - [x] Unsafe blocks (if any) have justifying comments - [x] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable ## Details - The [devcontainer CLI writes the `devcontainer.metadata` label as a bare JSON object](https://github.com/devcontainers/cli/issues/1054) when there is only one metadata entry (e.g. docker-compose devcontainer with a Dockerfile and no features) - Zed's `deserialize_metadata` only accepted a JSON array, causing deserialization to fail with `invalid type: map, expected a sequence` - This made it impossible to attach to existing docker-compose devcontainers created by the devcontainer CLI or VS Code The fix tries parsing as an array first, then falls back to parsing as a single object wrapped in a vec. This mirrors how the [devcontainer CLI itself reads the label](https://github.com/devcontainers/cli/blob/main/src/spec-node/imageMetadata.ts#L476-L493). An upstream fix has also been submitted: https://github.com/devcontainers/cli/pull/1199 ## Reproduction 1. Create a docker-compose devcontainer with a Dockerfile and no features: `.devcontainer/devcontainer.json`: ```json { "name": "repro", "dockerComposeFile": "docker-compose.yml", "service": "app", "remoteUser": "root" } ``` `.devcontainer/docker-compose.yml`: ```yaml services: app: build: context: . dockerfile: Dockerfile command: sleep infinity volumes: - ..:/workspace ``` `.devcontainer/Dockerfile`: ```dockerfile FROM ubuntu:24.04 ``` 2. `devcontainer up --workspace-folder .` 3. Open the folder in Zed, fails with metadata deserialization error Release Notes: - Fixed attaching to a devcontainer that has a single metadata element which was started with `devcontainer-cli` --- crates/dev_container/src/docker.rs | 38 +++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/crates/dev_container/src/docker.rs b/crates/dev_container/src/docker.rs index 99ce7422eee36d56e2bc53fd31d150fe2f41b16d..7931923b4219e33fa56e8fb2fb6b97c1ea89a750 100644 --- a/crates/dev_container/src/docker.rs +++ b/crates/dev_container/src/docker.rs @@ -487,10 +487,18 @@ where let s: Option = Option::deserialize(deserializer)?; match s { Some(json_string) => { + // The devcontainer metadata label can be either a JSON array (e.g. from + // image-based devcontainers) or a single JSON object (e.g. from + // docker-compose-based devcontainers created by the devcontainer CLI). + // Handle both formats. let parsed: Vec> = - serde_json_lenient::from_str(&json_string).map_err(|e| { - log::error!("Error deserializing metadata: {e}"); - serde::de::Error::custom(e) + serde_json_lenient::from_str(&json_string).or_else(|_| { + let single: HashMap = + serde_json_lenient::from_str(&json_string).map_err(|e| { + log::error!("Error deserializing metadata: {e}"); + serde::de::Error::custom(e) + })?; + Ok(vec![single]) })?; Ok(Some(parsed)) } @@ -936,6 +944,30 @@ mod test { assert_eq!(target_dir.unwrap(), "/workspaces/cli/".to_string()); } + #[test] + fn should_deserialize_object_metadata_from_docker_compose_container() { + // The devcontainer CLI writes metadata as a bare JSON object (not an array) + // when there is only one metadata entry (e.g. docker-compose with no features). + // See https://github.com/devcontainers/cli/issues/1054 + let given_config = r#" + { + "Id": "dc4e7b8ff4bf", + "Config": { + "Labels": { + "devcontainer.metadata": "{\"remoteUser\":\"ubuntu\"}" + } + } + } + "#; + let config = serde_json_lenient::from_str::(given_config).unwrap(); + + assert!(config.config.labels.metadata.is_some()); + let metadata = config.config.labels.metadata.unwrap(); + assert_eq!(metadata.len(), 1); + assert!(metadata[0].contains_key("remoteUser")); + assert_eq!(metadata[0]["remoteUser"], "ubuntu"); + } + #[test] fn should_deserialize_docker_compose_config() { let given_config = r#"