176

Could you please explain why if input argument of mutation is object it should be input type? I think much simpler just reuse type without providing id.

For example:

type Sample {
  id: String
  name: String
}

input SampleInput {
  name: String
}

type RootMutation {
  addSample(sample: Sample): Sample  # <-- instead of it should be
  addSample(sample: SampleInput): Sample
}

It's okay for small object, but when you have plenty of objects with 10+ properties in schema that'll become a burden.

Dmitrii Dushkin
  • 3,070
  • 4
  • 26
  • 37

4 Answers4

247

From the spec:

The GraphQL Object type (ObjectTypeDefinition)... is inappropriate for re‐use [as an input], because Object types can contain fields that define arguments or contain references to interfaces and unions, neither of which is appropriate for use as an input argument. For this reason, input objects have a separate type in the system.

That's the "official reason", but there's several practical reasons why you can't use an object type as an input object type or use an object type as an input object type:

Functionality

Object types and input object types both have fields, however those fields have different properties that reflect how these types are used by the schema. Your schema will potentially define arguments and some kind of resolver function for an object type's fields, but these properties don't make sense in an input context (i.e. you can't resolve an input object's field -- it already has an explicit value). Similarly, default values can only be provided for input object type fields, and not object type fields.

In other words, this may seem like duplication:

type Student {
  name: String
  grade: Grade
}

input StudentInput {
  name: String
  grade: Grade
}

But adding features specific to either object types or input object types makes it clear that they behave differently:

type Student {
  name(preferred: Boolean): String
  grade: Grade
}

input StudentInput {
  name: String
  grade: Grade = F
}

Type system limitations

Types in GraphQL are grouped into output types and input types.

Output types are types that may be returned as part of a response produced by a GraphQL service. Input types are types that are valid inputs for field or directive arguments.

There's overlap between these two groups (i.e. scalars, enums, lists and non-nulls). However, abstract types like unions and interfaces don't make sense in an input context and cannot be used as inputs. Separating object types and input object types allows you to ensure that an abstract type is never used where an input type is expected.

Schema design

When representing an entity in your schema, it's likely that some entities will indeed "share fields" between their respective input and output types:

type Student {
  firstName: String
  lastName: String
  grade: Grade
}

input StudentInput {
  firstName: String
  lastName: String
  grade: Grade
}

However, object types can (and in reality frequently do) model very complex data structures:

type Student {
  fullName: String!
  classes: [Class!]!
  address: Address!
  emergencyContact: Contact
  # etc
}

While these structures may translate into appropriate inputs (we create a Student, so we also pass in an object representing their address), often they do not -- i.e. maybe we need to specify the student's classes by class ID and section ID, not an object. Similarly, we may have fields that we want to return, but don't want to mutate, or vice versa (like a password field).

Moreover, even for relatively simple entities, we often have different requirements around nullability between object types and their "counterpart" input objects. Often we want to guarantee that a field will also be returned in a response, but we don't want to make the same fields required in our input. For example,

type Student {
  firstName: String!
  lastName: String!
}

input StudentInput {
  firstName: String
  lastName: String
}

Lastly, in many schemas, there's often not a one-to-one mapping between object type and input object type for a given entity. A common pattern is to utilize separate input object types for different operations to further fine-tune the schema-level input validation:

input CreateUserInput {
  firstName: String!
  lastName: String!
  email: String!
  password: String!
}

input UpdateUserInput {
  email: String
  password: String
}

All of these examples illustrate an important point -- while an input object type may mirror an object type some of the time, you're much less likely to see that in production schemas due to business requirements.

Daniel Rearden
  • 80,636
  • 11
  • 185
  • 183
  • 1
    Correct me if I'm wrong but in your example, the type `Grade` cannot be reused in the input `StudentInput`, right? You'll need to either inline fields in the input object or have a `GradeInput` input object. – Matt May 15 '20 at 08:44
  • 4
    @Matt Good question! In the above example, `Grade` is an enum type. Unlike with objects, scalar types (like String, Int, etc.) and enums types can be used as *both* input types *and* output types. – Daniel Rearden May 15 '20 at 10:32
  • 1
    This is so much clearer now -- if I'm understanding things correctly, in GraphQL there are both `Input Types` _and_ "output types", that each deal separately with incoming requests or outgoing responses. But, the GraphQL "output types" are actually never referred to as such, output types are simply called `Type`. Perhaps in a later version instead of using `type` and `input` it would be clearer to use `output` and `input`. Thanks for the explanation – FireDragon Nov 21 '21 at 17:18
  • @DanielRearden thank you for a great explanation. I have the same issue, but in my case the output type just extends the input type. I understand why the coupling between them can be an issue, but for my use case it will be good enough. Is there a way to define that the output type will extend the input type? – Roy Leibovitz Dec 28 '21 at 10:07
  • 1
    @RoyLeibovitz as outlined in the "Functionality" section above, fields on output types and input types each have different properties, so having an output type extend an input type (or vice versa) would be problematic and is not supported by the spec. Depending on the language, libraries and frameworks you're using, it may be _possible_ using some hacky method, but I would highly discourage doing so. – Daniel Rearden Dec 30 '21 at 01:56
  • We don't want to use any hacky methods, we are just wondering why is this not supported at all, I prefer to decide by myself if the usage of it is making sense or not. In my scenario, the output and input fields are almost identical, the output can extend the input with other fields and we could avoid this duplication. – Roy Leibovitz Jan 10 '22 at 14:27
  • Why not just use scalar types for input instead of an input type? – jacob Jul 01 '22 at 20:26
29

Jesse's comment is correct. For more formal answer, here is the excerpt from GraphQL documentation on input types:

The Object type defined above is inappropriate for re‐use here, because Objects can contain fields that express circular references or references to interfaces and unions, neither of which is appropriate for use as an input argument. For this reason, input objects have a separate type in the system.

UPDATE

Since posting it, I found that circular references actually are acceptable, so long as those are nilable (or else it would declare an infinite chain). But, still there are other limitations (e.g. interfaces) that seem to necessitate a separate type system for inputs.

LB2
  • 4,802
  • 19
  • 35
  • 5
    There's a bit more discussion here on why this restriction exists: https://github.com/graphql/graphql-js/issues/599 – Cam Jackson Dec 19 '17 at 06:52
0

Simply because you don't want to couple your public API to your Domain models or DB models. Incoming data is represented differently in response data. You don't want to reuse the same data type for incoming vs outcoming...or you'll completely tie your users to your domain or db model and that means anything you change can break your clients

What Input types are, are essentially RequestTypes. When you think of boundaries between the delivery mechanism (the client making the request) and the boundary between that and the business logic below your controller when you think about a Hexagonal Architecture for example.

PositiveGuy
  • 17,621
  • 26
  • 79
  • 138
0

I had the same debate. For me, the answer wasn't black or white. Sometimes, an object type is truly treated closely as a primitive throughout the entire schema. E.g. let's consider a Location type:

@ObjectType
class Location {
    @Field()
    lat: number
    @Field()
    lon: number
}

Which is ALWAYS treated as a property of some other object type, e.g.:

@ObjectType
class MyPlace {
    @Field(type => Location)
    location: Location
}

Meaning that when MyPlace is created/updated/queried, location is always passed as if it's any other string or number.

In that case, what's wrong with using Location as both an object and input type?

@ObjectType
@InputType('LocationInput')
class Location {
    @Field()
    lat: number
    @Field()
    lon: number
}
Sagi Mann
  • 2,967
  • 6
  • 39
  • 72