Repository: gleam-lang/gleam (rust)

Issue:
Tweak generated code for "Generate JSON encoder" action
Currently, one thing I like about the "Generate dynamic decoder" action is the fact that if you change the type that is being decoded, the decoder function will contain a compile error. For example, let's say you have this type:
```gleam
type Person {
  Person(name: String, age: Int)
}
```
The generated decoder will look like this:
```gleam
fn person_decoder() -> decode.Decoder(Person) {
  use name <- decode.field("name", decode.string)
  use age <- decode.field("age", decode.int)
  decode.success(Person(name:, age:))
}
```
Now let's say you want to add a new field, `job: String`. That decoder function will now error because you are trying to construct a `Person` without providing all the fields. This signals to the programmer that they need to delete the decoder and rerun the code action.
However, this is what the generared JSON encoder looks like for the above type currently.
```gleam
fn encode_person(person: Person) -> json.Json {
  json.object([
    #("name", json.string(person.name)),
    #("age", json.int(person.age)),
  ])
}
```
Even with a new field, this compiles fine. Nothing here is ensuring all fields are covered, so it's possible that someone will forget to update the encoder, and end up introducing a bug in the code.
To fix this, I propose a slight change in the generated code. Instead, it would look like this:
```gleam
fn encode_person(person: Person) -> json.Json {
  let Person(name:, age:) = person
  json.object([
    #("name", json.string(name)),
    #("age", json.int(age)),
  ])
}
```
This way, adding a new field will cause the function to fail to compile, and help people remember to update their encoders as well as decoders when the type changes.

Pull request description:
Tweak generated code for JSON encoders
Closes #4353

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 828b3100f..b04b3bd25 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -49,6 +49,42 @@
 
   ([Surya Rose](https://github.com/GearsDatapacks))
 
+- The function generated by the "Generate JSON encoder" code action has been
+  slightly modified so that it will now fail to compile if the type has new
+  fields added, ensuring the programmer remembers to re-run the code action.
+  For example, for this type:
+
+  ```gleam
+  type Person {
+    Person(name: String, age: Int)
+  }
+  ```
+
+  The following code used to be generated:
+
+  ```gleam
+  fn encode_person(person: Person) -> json.Json {
+    json.object([
+      #("name", json.string(person.name)),
+      #("age", json.int(person.age)),
+    ])
+  }
+  ```
+
+  But now, this code is generated:
+
+  ```gleam
+  fn encode_person(person: Person) -> json.Json {
+    let Person(name:, age:) = person
+    json.object([
+      #("name", json.string(name)),
+      #("age", json.int(age)),
+    ])
+  }
+  ```
+
+  ([Surya Rose](https://github.com/GearsDatapacks))
+
 ### Formatter
 
 ### Bug fixes
@@ -68,7 +104,7 @@
 - Fixed a bug where using the "Convert to pipe" code action on a function or
   record capture produces invalid code.
   ([Matias Carlander](https://github.com/matiascr))
-  
+
 
 ## v1.9.1 - 2025-03-10
 
diff --git a/compiler-core/src/language_server/code_action.rs b/compiler-core/src/language_server/code_action.rs
index 4b34eadda..95f04e21c 100644
--- a/compiler-core/src/language_server/code_action.rs
+++ b/compiler-core/src/language_server/code_action.rs
@@ -3407,7 +3407,23 @@ impl<'a> GenerateJsonEncoder<'a> {
         // require pattern matching on the argument as we can access all its fields
         // with the usual record access syntax.
         if rest.is_empty() {
-            return self.constructor_encoder(mode, first, custom_type.name.clone(), record_name, 2);
+            let encoder = self.constructor_encoder(mode, first, custom_type.name.clone(), 2)?;
+            let unpacking = if first.arguments.is_empty() {
+                ""
+            } else {
+                &eco_format!(
+                    "let {name}({fields}:) = {record_name}\n  ",
+                    name = first.name,
+                    fields = first
+                        .arguments
+                        .iter()
+                        .filter_map(|argument| {
+                            argument.label.as_ref().map(|(_location, label)| label)
+                        })
+                        .join(":, ")
+                )
+            };
+            return Some(eco_format!("{unpacking}{encoder}"));
         }
 
         // Otherwise we generate an encoder for a type with multiple constructors:
@@ -3416,17 +3432,21 @@ impl<'a> GenerateJsonEncoder<'a> {
         let mut branches = Vec::with_capacity(constructors_size);
         for constructor in iter::once(first).chain(rest) {
             let RecordConstructor { name, .. } = constructor;
-            let encoder = self.constructor_encoder(
-                mode,
-                constructor,
-                custom_type.name.clone(),
-                record_name.clone(),
-                4,
-            )?;
+            let encoder =
+                self.constructor_encoder(mode, constructor, custom_type.name.clone(), 4)?;
             let unpacking = if constructor.arguments.is_empty() {
                 ""
             } else {
-                "(..)"
+                &eco_format!(
+                    "({}:)",
+                    constructor
+                        .arguments
+                        .iter()
+                        .filter_map(|argument| {
+                            argument.label.as_ref().map(|(_location, label)| label)
+                        })
+                        .join(":, ")
+                )
             };
             branches.push(eco_format!("    {name}{unpacking} -> {encoder}"));
         }
@@ -3444,7 +3464,6 @@ impl<'a> GenerateJsonEncoder<'a> {
         mode: EncodingMode,
         constructor: &TypedRecordConstructor,
         type_name: EcoString,
-        record_name: EcoString,
         nesting: usize,
     ) -> Option<EcoString> {
         let json_module = self.printer.print_module(JSON_MODULE);
@@ -3461,7 +3480,7 @@ impl<'a> GenerateJsonEncoder<'a> {
         let mut encoder_printer =
             EncoderPrinter::new(&self.module.ast.names, type_name, self.module.name.clone());
 
-        // These aare the fields of the json object to encode.
+        // These are the fields of the json object to encode.
         let mut fields = Vec::with_capacity(constructor.arguments.len());
         if mode == EncodingMode::ObjectWithTypeTag {
             // Any needed type tag is always going to be the first field in the object
@@ -3476,7 +3495,7 @@ impl<'a> GenerateJsonEncoder<'a> {
                 label: RecordLabel::Labeled(label),
                 type_: &argument.type_,
             };
-            let encoder = encoder_printer.encode_field(&record_name, &field, nesting + 2);
+            let encoder = encoder_printer.encode_field(&field, nesting + 2);
             fields.push(encoder);
         }
 
@@ -3683,14 +3702,9 @@ impl<'a> EncoderPrinter<'a> {
         }
     }
 
-    fn encode_field(
-        &mut self,
-        record_name: &str,
-        field: &RecordField<'_>,
-        indent: usize,
-    ) -> EcoString {
+    fn encode_field(&mut self, field: &RecordField<'_>, indent: usize) -> EcoString {
         let field_name = field.label.variable_name();
-        let encoder = self.encoder_for(&format!("{record_name}.{field_name}"), field.type_, indent);
+        let encoder = self.encoder_for(&field_name, field.type_, indent);
 
         eco_format!(
             r#"{indent}#("{field_name}", {encoder})"#,


Test patch:
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder.snap
index 6381a21b8..0051ce33f 100644
--- a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder.snap
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder.snap
@@ -18,10 +18,11 @@ pub type Person {
 }
 
 fn encode_person(person: Person) -> json.Json {
+  let Person(name:, age:, height:, is_cool:) = person
   json.object([
-    #("name", json.string(person.name)),
-    #("age", json.int(person.age)),
-    #("height", json.float(person.height)),
-    #("is_cool", json.bool(person.is_cool)),
+    #("name", json.string(name)),
+    #("age", json.int(age)),
+    #("height", json.float(height)),
+    #("is_cool", json.bool(is_cool)),
   ])
 }
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_already_imported_module.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_already_imported_module.snap
index fa0f56312..e73cdca00 100644
--- a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_already_imported_module.snap
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_already_imported_module.snap
@@ -21,9 +21,10 @@ pub type Wibble {
 }
 
 fn encode_wibble(wibble: Wibble) -> json_encoding.Json {
+  let Wibble(a:, b:, c:) = wibble
   json_encoding.object([
-    #("a", json_encoding.int(wibble.a)),
-    #("b", json_encoding.float(wibble.b)),
-    #("c", json_encoding.string(wibble.c)),
+    #("a", json_encoding.int(a)),
+    #("b", json_encoding.float(b)),
+    #("c", json_encoding.string(c)),
   ])
 }
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_complex_types.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_complex_types.snap
index 29ce59a19..54b91c8e5 100644
--- a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_complex_types.snap
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_complex_types.snap
@@ -38,13 +38,14 @@ pub type Wibble(value) {
 }
 
 fn encode_wibble(wibble: Wibble(value)) -> json.Json {
+  let Wibble(maybe:, something:, map:, unknown:) = wibble
   json.object([
-    #("maybe", case wibble.maybe {
+    #("maybe", case maybe {
       option.None -> json.null()
       option.Some(value) -> json.int(value)
     }),
     #("something", todo as "Encoder for Something"),
-    #("map", json.dict(wibble.map, fn(string) { string }, json.array(_, json.float))),
-    #("unknown", json.array(wibble.unknown, todo as "Encoder for value")),
+    #("map", json.dict(map, fn(string) { string }, json.array(_, json.float))),
+    #("unknown", json.array(unknown, todo as "Encoder for value")),
   ])
 }
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_for_multi_variant_type.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_for_multi_variant_type.snap
index 73ba98261..e58e8d068 100644
--- a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_for_multi_variant_type.snap
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_for_multi_variant_type.snap
@@ -21,16 +21,16 @@ pub type Wibble {
 
 fn encode_wibble(wibble: Wibble) -> json.Json {
   case wibble {
-    Wibble(..) -> json.object([
+    Wibble(wibble:, next:) -> json.object([
       #("type", json.string("wibble")),
-      #("wibble", json.int(wibble.wibble)),
-      #("next", encode_wibble(wibble.next)),
+      #("wibble", json.int(wibble)),
+      #("next", encode_wibble(next)),
     ])
-    Wobble(..) -> json.object([
+    Wobble(wobble:, text:, values:) -> json.object([
       #("type", json.string("wobble")),
-      #("wobble", json.float(wibble.wobble)),
-      #("text", json.string(wibble.text)),
-      #("values", json.array(wibble.values, json.bool)),
+      #("wobble", json.float(wobble)),
+      #("text", json.string(text)),
+      #("values", json.array(values, json.bool)),
     ])
   }
 }
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_for_multi_variant_type_multi_word_name.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_for_multi_variant_type_multi_word_name.snap
index 27ddd998f..4a322f44c 100644
--- a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_for_multi_variant_type_multi_word_name.snap
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_for_multi_variant_type_multi_word_name.snap
@@ -23,20 +23,20 @@ pub type Wibble {
 
 fn encode_wibble(wibble: Wibble) -> json.Json {
   case wibble {
-    OneTwoThree(..) -> json.object([
+    OneTwoThree(wibble:, next:) -> json.object([
       #("type", json.string("one_two_three")),
-      #("wibble", json.int(wibble.wibble)),
-      #("next", encode_wibble(wibble.next)),
+      #("wibble", json.int(wibble)),
+      #("next", encode_wibble(next)),
     ])
-    FourFive(..) -> json.object([
+    FourFive(wobble:, text:, values:) -> json.object([
       #("type", json.string("four_five")),
-      #("wobble", json.float(wibble.wobble)),
-      #("text", json.string(wibble.text)),
-      #("values", json.array(wibble.values, json.bool)),
+      #("wobble", json.float(wobble)),
+      #("text", json.string(text)),
+      #("values", json.array(values, json.bool)),
     ])
-    SixSevenEight(..) -> json.object([
+    SixSevenEight(one_two:) -> json.object([
       #("type", json.string("six_seven_eight")),
-      #("one_two", json.float(wibble.one_two)),
+      #("one_two", json.float(one_two)),
     ])
   }
 }
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_for_variants_with_mixed_fields.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_for_variants_with_mixed_fields.snap
index d0ac3c197..70dafb128 100644
--- a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_for_variants_with_mixed_fields.snap
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_for_variants_with_mixed_fields.snap
@@ -24,10 +24,10 @@ fn encode_wibble(wibble: Wibble) -> json.Json {
     Wibble -> json.object([
       #("type", json.string("wibble")),
     ])
-    Wobble(..) -> json.object([
+    Wobble(field:, field1:) -> json.object([
       #("type", json.string("wobble")),
-      #("field", json.string(wibble.field)),
-      #("field1", json.int(wibble.field1)),
+      #("field", json.string(field)),
+      #("field1", json.int(field1)),
     ])
   }
 }
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_list_of_tuples.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_list_of_tuples.snap
index 97c894960..07c2ca903 100644
--- a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_list_of_tuples.snap
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_list_of_tuples.snap
@@ -18,8 +18,9 @@ pub type Wibble {
 }
 
 fn encode_wibble(wibble: Wibble) -> json.Json {
+  let Wibble(values:) = wibble
   json.object([
-    #("values", json.array(wibble.values, fn(value) {
+    #("values", json.array(values, fn(value) {
       json.preprocessed_array([
         json.int(value.0),
         json.string(value.1),
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_recursive_type.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_recursive_type.snap
index 18383bf43..ac62bb141 100644
--- a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_recursive_type.snap
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_recursive_type.snap
@@ -22,9 +22,10 @@ pub type LinkedList {
 }
 
 fn encode_linked_list(linked_list: LinkedList) -> json.Json {
+  let LinkedList(value:, next:) = linked_list
   json.object([
-    #("value", json.int(linked_list.value)),
-    #("next", case linked_list.next {
+    #("value", json.int(value)),
+    #("next", case next {
       option.None -> json.null()
       Some(value) -> encode_linked_list(value)
     }),
diff --git a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_tuple.snap b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_tuple.snap
index 5e4137c1c..9f6678283 100644
--- a/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_tuple.snap
+++ b/compiler-core/src/language_server/tests/snapshots/gleam_core__language_server__tests__action__generate_json_encoder_tuple.snap
@@ -18,13 +18,14 @@ pub type Wibble {
 }
 
 fn encode_wibble(wibble: Wibble) -> json.Json {
+  let Wibble(tuple:) = wibble
   json.object([
     #("tuple", json.preprocessed_array([
-      json.int(wibble.tuple.0),
-      json.float(wibble.tuple.1),
+      json.int(tuple.0),
+      json.float(tuple.1),
       json.preprocessed_array([
-        json.string(wibble.tuple.2.0),
-        json.bool(wibble.tuple.2.1),
+        json.string(tuple.2.0),
+        json.bool(tuple.2.1),
       ]),
     ])),
   ])