Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
release type: patch
social_messages:
x: >-
Strawberry {version} is out! This release fixes schema codegen silently
renaming fields whose GraphQL names don't survive camel-casing. 🍓
https://strawberry.rocks/release/{version}
linkedin: >-
Strawberry {version} is out. This release fixes schema codegen so that
GraphQL field names such as `some_field` or `allowCustomExportURL` keep
their original names in the generated schema instead of being silently
renamed.
---

This release fixes schema codegen silently renaming fields whose GraphQL names
are not reproduced by camel-casing.

Strawberry derives the GraphQL name of a field by camel-casing its Python name,
so generating `some_field` from a GraphQL field named `some_field` produced a
schema exposing `someField` instead. The same happened to names containing
acronyms, such as `allowCustomExportURL`, which came back as
`allowCustomExportUrl`.

Codegen now adds an explicit alias whenever camel-casing would not give the
original name back:

```python
@strawberry.type
class Example:
some_field: int | None = strawberry.field(name="some_field")
allow_custom_export_url: bool = strawberry.field(name="allowCustomExportURL")
```

Fields whose GraphQL names convert to the same Python name (for example
`someField` and `some_field`) are also no longer silently dropped.
22 changes: 18 additions & 4 deletions strawberry/schema_codegen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
NullValueNode,
)

from strawberry.utils.str_converters import to_snake_case
from strawberry.utils.str_converters import to_camel_case, to_snake_case

if TYPE_CHECKING:
from graphql.language.ast import ConstDirectiveNode
Expand Down Expand Up @@ -338,13 +338,25 @@ def _get_field(
imports: set[Import],
*,
is_input_field: bool = False,
used_names: set[str],
) -> cst.SimpleStatementLine:
name = to_snake_case(field.name.value)
alias: str | None = None
graphql_name = field.name.value
name = to_snake_case(graphql_name)

if keyword.iskeyword(name):
name = f"{name}_"
alias = field.name.value

# Two GraphQL names can convert to the same Python name (e.g. `someField`
# and `some_field`), which would silently overwrite the previous field.
while name in used_names:
name += "_"

used_names.add(name)

# Strawberry derives the GraphQL name by camel-casing the Python name, so an
# explicit alias is needed whenever that would not give the original name
# back, e.g. for `some_field`, `URL` or names aliased above.
alias = graphql_name if to_camel_case(name) != graphql_name else None

# For input types, wrap nullable fields in strawberry.Maybe[...]
wrap_in_maybe = is_input_field and _is_nullable(field.type)
Expand Down Expand Up @@ -571,6 +583,7 @@ def _get_class_definition(

is_input_type = isinstance(definition, InputObjectTypeDefinitionNode)

used_names: set[str] = set()
class_definition = cst.ClassDef(
name=cst.Name(definition.name.value),
body=cst.IndentedBlock(
Expand All @@ -580,6 +593,7 @@ def _get_class_definition(
is_apollo_federation,
imports,
is_input_field=is_input_type,
used_names=used_names,
)
for field in definition.fields
]
Expand Down
54 changes: 52 additions & 2 deletions tests/schema_codegen/test_names.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,58 @@ def test_converts_names_to_snake_case():
@strawberry.type
class Example:
some_field: str
allow_custom_export_url: bool
allow_insecure_tls: bool
allow_custom_export_url: bool = strawberry.field(name="allowCustomExportURL")
allow_insecure_tls: bool = strawberry.field(name="allowInsecureTLS")
"""
).strip()

assert codegen(schema).strip() == expected


def test_keeps_graphql_names_that_do_not_survive_camel_casing():
# Strawberry camel-cases Python names to get the GraphQL name, so names that
# are not reproduced by that conversion need an explicit alias.
schema = """
type Example {
some_field: Int
URL: String
}
"""

expected = textwrap.dedent(
"""
from __future__ import annotations
import strawberry

@strawberry.type
class Example:
some_field: int | None = strawberry.field(name="some_field")
url: str | None = strawberry.field(name="URL")
"""
).strip()

assert codegen(schema).strip() == expected


def test_handles_names_converting_to_the_same_python_name():
# `someField` and `some_field` both convert to `some_field`; the second one
# must not overwrite the first.
schema = """
type Example {
someField: String
some_field: Int
}
"""

expected = textwrap.dedent(
"""
from __future__ import annotations
import strawberry

@strawberry.type
class Example:
some_field: str | None
some_field_: int | None = strawberry.field(name="some_field")
"""
).strip()

Expand Down
Loading