Skip to content

Add connectors ->keysToCamelCase arrow method#9131

Open
andrewmcgivery wants to merge 7 commits intodevfrom
am/connectors_to_camelcase
Open

Add connectors ->keysToCamelCase arrow method#9131
andrewmcgivery wants to merge 7 commits intodevfrom
am/connectors_to_camelcase

Conversation

@andrewmcgivery
Copy link
Copy Markdown
Contributor

@andrewmcgivery andrewmcgivery commented Apr 3, 2026

Adds a new ->keysToCamelCase method to convert all the properties of an object from various cases to camelCase. Shallow by default but can be made recursive with -> keysToCamelCase(true).

Example usage:

$->keysToCamelCase {
        propertyA
        propertyB
        propertyC
      }
image

Checklist

Complete the checklist (and note appropriate exceptions) before the PR is marked ready-for-review.

  • PR description explains the motivation for the change and relevant context for reviewing
  • PR description links appropriate GitHub/Jira tickets (creating when necessary)
  • Changeset is included for user-facing changes
  • Changes are compatible1
  • Documentation2 completed
  • Performance impact assessed and acceptable
  • Metrics and logs are added3 and documented
  • Tests added and passing4
    • Unit tests
    • Integration tests
    • Manual tests, as necessary

Exceptions

Note any exceptions here

Notes

Footnotes

  1. It may be appropriate to bring upcoming changes to the attention of other (impacted) groups. Please endeavour to do this before seeking PR approval. The mechanism for doing this will vary considerably, so use your judgement as to how and when to do this.

  2. Configuration is an important part of many changes. Where applicable please try to document configuration examples.

  3. A lot of (if not most) features benefit from built-in observability and debug-level logs. Please read this guidance on metrics best-practices.

  4. Tick whichever testing boxes are applicable. If you are adding Manual Tests, please document the manual testing (extensively) in the Exceptions.

@apollo-librarian
Copy link
Copy Markdown
Contributor

apollo-librarian bot commented Apr 3, 2026

✅ Docs preview has no changes

The preview was not built because there were no changes.

Build ID: 9589eea08382d53f65951245
Build Logs: View logs


✅ AI Style Review — No Changes Detected

No MDX files were changed in this pull request.

Review Log: View detailed log

This review is AI-generated. Please use common sense when accepting these suggestions, as they may not always be accurate or appropriate for your specific context.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 3, 2026

@andrewmcgivery, please consider creating a changeset entry in /.changesets/. These instructions describe the process and tooling.

Copy link
Copy Markdown
Member

@benjamn benjamn left a comment

Choose a reason for hiding this comment

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

Some quick thoughts:

  • I think we should call this ->keysToCamelCase since we're only renaming the keys (and it's also a bit shorter)
  • It's surprising to me that the recursive version is the default, though I see why it's useful. Can we swap the default but keep the option to rename recursively?
  • Is it problematic that ->keysToCamelCase does not specify which casing style it expects as the input? Maybe we need ->keysFromSnakeToCamelCase, ->keysFromCamelToSnakeCase and so forth?
  • If that seems like too much, we could let the input/output styles be passed in as arguments, like
->convertKeyCase("snake", "camel")
->convertKeyCase("kebab", "snake")
  • I think we can safely worry about input shapes later. The subselection after the rename will determine the output shape for now.

@benjamn
Copy link
Copy Markdown
Member

benjamn commented Apr 7, 2026

One more thought: it could be expensive/undesirable to rename all keys in a large object, so we might want some way to specify a subset of keys that we want to rename?

Possible sketch:

$->convertKeyCase("snake", "camel", ["property_a", "property_b"]) {
  id
  name
  propertyA
  propertyB
}

@andrewmcgivery
Copy link
Copy Markdown
Contributor Author

Some quick thoughts:

  • I think we should call this ->keysToCamelCase since we're only renaming the keys (and it's also a bit shorter)
  • It's surprising to me that the recursive version is the default, though I see why it's useful. Can we swap the default but keep the option to rename recursively?
  • Is it problematic that ->keysToCamelCase does not specify which casing style it expects as the input? Maybe we need ->keysFromSnakeToCamelCase, ->keysFromCamelToSnakeCase and so forth?
  • If that seems like too much, we could let the input/output styles be passed in as arguments, like
->convertKeyCase("snake", "camel")
->convertKeyCase("kebab", "snake")
  • I think we can safely worry about input shapes later. The subselection after the rename will determine the output shape for now.
  • I can get behind keysToCamelCase... matches our future keys function well.
  • My thought process was that most of the time, the recursive version is what users are going to want because graphql field are always camelCase and the purpose of this function is to reduce the amount of field renaming that has to be done. If we do swap it, I think it becomes a tyranny of the defaults problem where we usually want the default to match what users are going to want to do 80% of the time. That said, I think typically most functions in the wild are not recursive by default so I'm willing to swap it on this one if we're okay with the tradeoff that users are probably most of the time going to be typing more as a result.
  • I don't think so. i think its implied (and we can document) that this hits everything... and given this is how these functions typically work, including the underlying package that we're using, I don't see this as a problem. If we need to introduce more granular versions in the future, I think we could do that, but the immediate user problem we're solving I think this is the shortest path and exactly what they are looking for.

@andrewmcgivery
Copy link
Copy Markdown
Contributor Author

One more thought: it could be expensive/undesirable to rename all keys in a large object, so we might want some way to specify a subset of keys that we want to rename?

Possible sketch:

$->convertKeyCase("snake", "camel", ["property_a", "property_b"]) {
  id
  name
  propertyA
  propertyB
}

Given the user's stated problem is that they do not want to have to manually rename all of the properties in large objects by hand, I would assume the behaviour that they want from the function is to rename all of the properties. 😆

Having to type out the property to rename and the new property name like this defeats the purpose of why we're creating this function in the first place.

Typing this:

$->convertKeyCase(["property_a", "property_b"]) {
  id
  name
  propertyA
  propertyB
}

Is just as much boilerplate as this:

{
  id
  name
  propertyA: property_a
  propertyB: property_b
}

The purpose of this function is that the user does not want to type out the property names. :)

@andrewmcgivery
Copy link
Copy Markdown
Contributor Author

@benjamn to answer your question on slack, the function uses the heck crate internally which already detects all the common cases automatically. :)

https://crates.io/crates/heck

@benjamn
Copy link
Copy Markdown
Member

benjamn commented Apr 7, 2026

You wouldn't have to specify specific property names in most cases, but I think developers are going to find themselves in situations where they don't want all properties renamed, but only certain ones, and they will want the ability to make the renaming more precise. I do not agree this defeats the purpose of the function.

I'd still like to hear more about how we're going to infer the input style, so we can have ->keysToCamelCase rather than ->convertKeyCase("snake", "camel"). Does every case really work without ambiguity?

@andrewmcgivery
Copy link
Copy Markdown
Contributor Author

You wouldn't have to specify specific property names in most cases, but I think developers are going to find themselves in situations where they don't want all properties renamed, but only certain ones, and they will want the ability to make the renaming more precise. I do not agree this defeats the purpose of the function.

I'd still like to hear more about how we're going to infer the input style, so we can have ->keysToCamelCase rather than ->convertKeyCase("snake", "camel"). Does every case really work without ambiguity?

If I only want to rename specific properties, why would I use this function at all when I can just use our normal renaming syntax?

@benjamn
Copy link
Copy Markdown
Member

benjamn commented Apr 7, 2026

Maybe because you want most of your snake case properties to be renamed to camel case, except for one? If ->keysToCamelCase renames everything, you'd have to go backwards a bit:

$->keysToCamelCase {
  id
  name
  propertyA
  propertyB
  exceptional_property: exceptionalProperty
}

I guess that's fine?

@andrewmcgivery
Copy link
Copy Markdown
Contributor Author

Maybe because you want most of your snake case properties to be renamed to camel case, except for one? If ->keysToCamelCase renames everything, you'd have to go backwards a bit:

$->keysToCamelCase {
  id
  name
  propertyA
  propertyB
  exceptional_property: exceptionalProperty
}

I guess that's fine?

That definitely seems like an edge case... I'd prefer to design for the vast majority of the time:

  1. The best practice and expected in graphql is camelCase keys
  2. The user is asking to rename everything to camelCase

And yea, they've always got that escape hatch in the mapping if for some odd reason they want to rename back lol

@andrewmcgivery andrewmcgivery changed the title Add connectors ->propertiesToCamelCase arrow method Add connectors -> keysToCamelCase arrow method Apr 7, 2026
@andrewmcgivery andrewmcgivery changed the title Add connectors -> keysToCamelCase arrow method Add connectors ->keysToCamelCase arrow method Apr 7, 2026
@andrewmcgivery andrewmcgivery marked this pull request as ready for review April 8, 2026 18:12
@andrewmcgivery andrewmcgivery requested review from a team as code owners April 8, 2026 18:12
Comment on lines +224 to +233
// Determine recursive flag from literal argument if possible
let recursive = if let Some(first_arg) = method_args.and_then(|args| args.args.first()) {
use crate::connectors::json_selection::lit_expr::LitExpr;
match first_arg.as_ref() {
LitExpr::Bool(b) => *b,
_ => false, // Default to false for non-literal args
}
} else {
false
};
Copy link
Copy Markdown
Member

@benjamn benjamn Apr 9, 2026

Choose a reason for hiding this comment

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

Just noting this struggles with variables/expressions like obj->keysToCamelCase($args.recursive)? I see how that's not so helpful for shape computation, though, since you don't know what the value will be at runtime, so unfortunately I don't have a good solution (you're already doing the right thing in keys_to_camel_case_method).

Would it be any easier to just have two methods, ->keysToCamelCase and ->keysToCamelCaseDeep? Then there's no argument so it can't be an expression?

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.

I see what you mean... in the method we actually allow an expression with apply_to_path but here we're assuming it's a boolean or default to false.

It's probably an unlikely scenario but it is definitely strange and inconsistent. In practice the recursive argument (probably) cannot or will not be dynamic because it is (probably) being used to map to schema, which is fixed. So I would say this is a pretty big edge case given the intended usage. But I don't love this.

The more I'm thinking about it as I'm typing this though, I suspect the easiest is having two methods... but I think I'm going to go with ->keysToCamelCase (shallow version) and ->keysToCamelCaseDeep.

Good call!

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!

Copy link
Copy Markdown
Member

@benjamn benjamn left a comment

Choose a reason for hiding this comment

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

Here are some potential issues that Claude flagged, both fixable:

1. Collision error message reports the wrong key

In transform_keys, the collision error tries to show which original keys collided:

new_map.iter()
    .find(|(k, _)| k.as_str() == camel_key)
    .map(|(k, _)| k.as_str())
    .unwrap_or("")

But new_map is keyed by the already-converted camelCase keys, so this lookup just finds camel_key itself. Given {"foo_bar": 1, "fooBar": 2}, the error reads:

"fooBar" and "fooBar" both map to "fooBar"

…when it should say "foo_bar" and "fooBar". The original key is lost after insertion. A fix would be to track a camel_key → original_key side-map so you can report the source key that caused the first insertion.

The test for this (should_warn_on_key_collision) only asserts contains("key collision"), so it doesn't catch the problem.

2. heck strips leading underscores — potentially surprising in GraphQL contexts

heck's to_lower_camel_case strips leading (and trailing) underscores:

Input Output
_id id
__typename typename
_private_field privateField

__typename is a GraphQL introspection field, and _id is common in MongoDB/REST responses — silently dropping the underscores could break downstream selections or cause confusing data loss.

A few options:

  • Preserve leading underscores by stripping them before conversion and prepending them back after
  • Emit a warning/error when a key's leading underscores would be stripped
  • At minimum, document the behavior prominently so users know what to expect

@andrewmcgivery
Copy link
Copy Markdown
Contributor Author

Here are some potential issues that Claude flagged, both fixable:

1. Collision error message reports the wrong key

In transform_keys, the collision error tries to show which original keys collided:

new_map.iter()
    .find(|(k, _)| k.as_str() == camel_key)
    .map(|(k, _)| k.as_str())
    .unwrap_or("")

But new_map is keyed by the already-converted camelCase keys, so this lookup just finds camel_key itself. Given {"foo_bar": 1, "fooBar": 2}, the error reads:

"fooBar" and "fooBar" both map to "fooBar"

…when it should say "foo_bar" and "fooBar". The original key is lost after insertion. A fix would be to track a camel_key → original_key side-map so you can report the source key that caused the first insertion.

The test for this (should_warn_on_key_collision) only asserts contains("key collision"), so it doesn't catch the problem.

2. heck strips leading underscores — potentially surprising in GraphQL contexts

heck's to_lower_camel_case strips leading (and trailing) underscores:

Input Output
_id id
__typename typename
_private_field privateField
__typename is a GraphQL introspection field, and _id is common in MongoDB/REST responses — silently dropping the underscores could break downstream selections or cause confusing data loss.

A few options:

  • Preserve leading underscores by stripping them before conversion and prepending them back after
  • Emit a warning/error when a key's leading underscores would be stripped
  • At minimum, document the behavior prominently so users know what to expect

First one is now fixed!

Second one I think is a red herring because like we discussed earlier, the "best practice" in graphql is camelCase. Including the underscores in graphql fields is usually for avery specific reason or for something internal, like __typename.

For the 3 examples given...

  • _id to id I think is expected and desired behaviour. In the rare case that this isn't, they have the escape hatch that we discussed above
  • A REST API should never return __typename. That is set by us in GraphQL.
  • _private_field... again probably an edge case that they would want to manually override with something like @inaccessible anyways... and again, they have the escape hatch if they need it.

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