Skip to content

feat(graphql): support flattened response fields - #300

Open
amnn wants to merge 1 commit into
amnn/gql-macro-shadowfrom
amnn/gql-flatten
Open

feat(graphql): support flattened response fields#300
amnn wants to merge 1 commit into
amnn/gql-macro-shadowfrom
amnn/gql-flatten

Conversation

@amnn

@amnn amnn commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

Description

The Response derive previously required every struct field to specify a path. This made it awkward to compose reusable response projections that read different fields from the same GraphQL response root.

Add #[field(flatten)] for fields that should receive the complete response value instead of a narrowed value:

#[derive(Response)]
struct ChainInfo {
    #[field(path = "chainIdentifier")]
    chain_id: String,
}

#[derive(Response)]
struct ResponseData {
    #[field(flatten)]
    chain: ChainInfo,
}

The derive now generates a borrowed extract(&serde_json::Value) method and keeps from_value(serde_json::Value) as an owned convenience wrapper. Flattened fields recursively call extract, so multiple projections can share the same response without cloning the entire JSON value. GraphQL union responses use the same borrowed extraction path for their inner variants.

flatten and path are mutually exclusive; combining them produces a compile-time error at the flatten attribute.

Test Plan

Adds extraction coverage for multiple flattened fields receiving the same root value, an extract-only field type, and a flattened field named value. Adds a compile-fail test for combining flatten with path.

$ cargo nextest run -p sui-graphql-macros

@amnn
amnn requested a review from bmwill as a code owner August 14, 2026 16:50
@amnn amnn self-assigned this Aug 14, 2026
@amnn
amnn requested a review from tpham-mysten August 14, 2026 16:54
@amnn
amnn force-pushed the amnn/gql-flatten branch 2 times, most recently from 1b090b1 to a83ee02 Compare August 14, 2026 16:58
@amnn

amnn commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator Author

field(flatten) is particularly useful when dealing with fragments -- it becomes possible to define a Rust type that corresponds to the fragment, and then flatten that into the field that the fragment has been spread into:

fragment Metadata on IObject {
  storageRebate
  previousTransaction { digest }
}
#[derive(Response)] 
#[graphql(root_type = "Object")]
struct Object {
  #[field(flatten)]
  metadata: Metadata
}

#[derive(Response)]
#[graphql(root_type = "DynamicField")]
struct DynamicField {
  #[field(flatten)]
  metadata: Metadata
}

#[derive(Response)]
#[graphql(root_type = "IObject")]
struct Metadata {
  #[field(path = "storageRebate")]
  storage_rebate: UInt53

  #[field(path = "previousTransaction.digest")]
  previous_transaction: Digest
}

The `Response` derive previously required every struct field to specify a
path. This made it awkward to compose reusable response projections that
read different fields from the same GraphQL response root.

Add `#[field(flatten)]` for fields that should receive the complete response
value instead of a narrowed value:

```rust
#[derive(Response)]
struct ChainInfo {
    #[field(path = "chainIdentifier")]
    chain_id: String,
}

#[derive(Response)]
struct ResponseData {
    #[field(flatten)]
    chain: ChainInfo,
}
```

The derive now generates a borrowed `extract(&serde_json::Value)` method and
keeps `from_value(serde_json::Value)` as an owned convenience wrapper.
Flattened fields recursively call `extract`, so multiple projections can
share the same response without cloning the entire JSON value. GraphQL union
responses use the same borrowed extraction path for their inner variants.

`flatten` and `path` are mutually exclusive; combining them produces a
compile-time error at the `flatten` attribute.

## Test Plan

Adds extraction coverage for multiple flattened fields receiving the same
root value, an `extract`-only field type, and a flattened field named `value`.
Adds a compile-fail test for combining `flatten` with `path`.

```
$ cargo test -p sui-graphql-macros
$ cargo clippy -p sui-graphql-macros --all-features --all-targets -- -D warnings
$ cargo test -p sui-graphql --no-run
```

@tpham-mysten tpham-mysten left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @amnn!

The fragment use case is a good motivation for it. Just a thing that flatten is now not compile-time safety. If client defines an incompatible object like metadata: RandomObject, it can be failed in runtime. It seems an intended behavior from your test with ExtractOnly object.

I'm thinking it would be ideal to enforce a stricter rule on top of this (maybe in a follow-up PR): the type used in flatten must declare a root_type, and that root_type must be compatible with the parent root_type.

The idea is that from the parent root_type we can compute the list of compatible types from the schema (the type itself, any interface it implements, and any union it belongs to). Then we check which root_type the flattened type declares and validate it against that list.

If users want to opt-out to define a custom ExtractOnly object, they can use skip_schema_validation

I just asked Claude to put up a draft in #303 to show what this would look like.

WDYT?

@amnn

amnn commented Aug 18, 2026

Copy link
Copy Markdown
Collaborator Author

Nice, yes, the extra validation looks good, thanks for that @tpham-mysten -- I've commented directly on that PR as well.

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