Skip to content

Conversation

@isaac-uptrust
Copy link

@isaac-uptrust isaac-uptrust commented Aug 21, 2025

The Generic-derived schema for polymorphic datatypes is incorrect. The schema for a datatype likedata X a = X{ field :: a } has field as a required property. This means that the schema for X (Maybe something) also has field marked as a required field. The optionality of the Maybe is forgotten. A service relying on the erroneous schema would expect a non-nullable value, but a service sending X (Maybe something) values can send null in the field propety, but a service relying on the erroneous schema will expect no null in field, leading to a runtime error.

Given data X a = X{ field :: a }, the Generic-derived schema must mark field as required, because it needs to work for all a. Therefore, instance ToSchema a => ToSchema (Maybe a) has to generate a nullable schema to be correct. Then the schema for X (Maybe something) will have required property field with a nullable schema for something.

This change changes the ToSchema instance for Maybe a. The schema for Maybe a is prefixed with Maybe_ to distinguish it from the schema for a. Without this, declareSchemaRef will generate the same reference for an a or a Maybe a. When an API uses an a and a Maybe a, only one of the two schemas will make it into the components.schemas section (because of the name conflict). This can lead to a schema that says it produces non-nullable values, but may actually produce nulls.

This problem could also be solved by changing declareSchemaRef to strip the nullable property from the declared schema and return the nullability value alongside the Referenced Schema. This would increase sharing (you wouldn't have near duplicate schemas X and Maybe_X), and I think it would be cleaner, but at the cost of forcing downstream libraries (such as servant-openapi3) to explicitly handle that nullability information. I chose the solution that's compatible with current downstream code.

I think this supercedes the previous attempts at fixing Maybe:

@isaac-uptrust isaac-uptrust force-pushed the ie/fix-maybe-schemas branch 3 times, most recently from 77fbf18 to f5075ee Compare August 21, 2025 05:24
The `Generic`-derived schema for polymorphic datatypes is incorrect. The schema for a datatype like
`data X a = X{ field :: a }` has `field` as a required property.
This means that the schema for `X (Maybe something)` also has `field` marked
as a required field.
The optionality of the `Maybe` is forgotten. A service relying on the erroneous schema would
expect a non-nullable value, but
The service sending `X (Maybe something)` values can send `null` in the `field` propety, but the
service relying on the erroneous schema will expect no `null` in `field`, leading to a runtime
error.

Given `data X a = X{ field :: a }`, the `Generic`-derived schema *has* to mark `field` as required,
because it must work for all `a`. Therefore, `instance ToSchema a => ToSchema (Maybe a)` has to
generate a `nullable` schema to be correct. Then the schema for `X (Maybe something)` will have
required property `field` with a `nullable` schema for `something`.
See the previous commit for an explanation of the problem.

This change also rules out `ToSchema (Maybe (Maybe _))` for correctness.

---

The schema for `Maybe a` is prefixed with `Maybe_` to distinguish it from
the schema for `a`. Without this, `declareSchemaRef` will generate the
same reference for an `a` or a `Maybe a`. When an API uses an `a` and a
`Maybe a`, only one of the two schemas will make it into the
`components.schemas` section (because of the name conflict). This can
lead to a schema that says it produces non-nullable values, but may
actually produce nulls.

This problem could also be solved by changing `declareSchemaRef` to
strip the `nullable` property from the declared schema and return the
nullability value alongside the `Referenced Schema`. This would increase
sharing (you wouldn't have near duplicate schemas `X` and `Maybe_X`), with
the cost of forcing downstream libraries (such as `servant-openapi3`) to
explicitly handle that nullability information. I chose the solution that's
compatible with current downstream code.
@chris-martin
Copy link

Clever use of overlapping instances. Love 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