Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Handle "type" being an array of strings in JSON schema converter #423

Merged
merged 2 commits into from
Feb 12, 2024

Conversation

adrianisk
Copy link
Contributor

@adrianisk adrianisk commented Jan 29, 2024

Summary

Fixes #412

Updates to handle the case where a type in JSON schema is an array of strings, like {"type": ["null", "string", "boolean", "number"]}. From the JSON schema docs

The type keyword may either be a string or an array:

If it's a string, it is the name of one of the basic types above.
If it is an array, it must be an array of strings, where each string is the name of one of the basic types, and each element is unique.

Details

I added tests to make sure nullable/optional types are converted correctly, and that properties of an object that are both not required & a union with null ({"type": ["null", "string"]}) are not made "double nullable".

@@ -65,12 +69,22 @@ def _parse(
self.json_registry = resource @ self.json_registry
extra_attrs["alias"] = alias_strategy(resource_id)

# Special handling for "type" defined as a list of strings like
# {"type": ["string", "boolean"]}
if "type" in json_schema and isinstance(json_schema["type"], list):
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this can be put inside the match statement. Something like:

case {"type": list(type_list)}:
  types = [self._parse(s, alias_strategy) for s in type_list]
  return UnionType(types, **extra_attrs)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call

# Make type nullable if it's not required, but avoid double nullable types
if (
name not in json_schema.get("required", [])
and not field.is_nullable()
Copy link
Contributor

Choose a reason for hiding this comment

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

I think there's a nuance that's not captured here. is_nullable checks UnionType with a NullType in it. It's not checking if there's a default value. Without a default value, the field is still required (a value must be set, even if it's null).

A JSON schema field with {"type": ["string", "null"]} that's not listed in required should be optional (i.e. it should have a default null, even if it's not set explicitly in the JSON schema). I don't think current logic accounts for this.

Copy link
Contributor Author

@adrianisk adrianisk Feb 6, 2024

Choose a reason for hiding this comment

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

Ah good catch, you are correct. Updated the logic here, and the relevant test

tests/unit/converters/test_json_schema.py Outdated Show resolved Hide resolved
name="required_nullable_with__default",
default="default_value",
),
UnionType([NullType(), StringType()], name="nullable_no_default"),
Copy link
Contributor

Choose a reason for hiding this comment

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

I believe a nullable_no_default should have defualt=None set since it's not listed in required. See my comment above.

tests/unit/converters/test_json_schema.py Outdated Show resolved Hide resolved
@criccomini
Copy link
Contributor

@adrianisk checking in here

@adrianisk
Copy link
Contributor Author

adrianisk commented Feb 6, 2024

Heyo, wrapping up a project that needs to get done today, then I'll take a look at this!

Edit: or first thing tomorrow 😪

Copy link
Contributor

@criccomini criccomini left a comment

Choose a reason for hiding this comment

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

Excellent. One nit and one bug. :) I think we should be good to merge after!

:return: True if the type is nullable.
"""

return isinstance(self, UnionType) and NullType() in self.types
Copy link
Contributor

Choose a reason for hiding this comment

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

This logic needs to be tweaked slightly. NullType() in self.types won't work in all cases because __eq__ checks docs, alias, logic, and extra_attrs, which might vary between NullTypes. See this code where I ran across the same issue when adding SQLite client/converter.

I believe the right solution is to search self.types for an isinstance(NullType). That's what I did in my PR.

field = field.make_nullable()
if not field.is_nullable():
field = field.make_nullable()
if "default" not in field.extra_attrs:
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Maybe a comment here to explain why you're setting default even though it's already set in make_nullable. It's the right logic, since is_nullable doesn't guarantee a default of none is set, but it's worth noting since it's subtle.

@criccomini criccomini merged commit 22d0e73 into gabledata:main Feb 12, 2024
3 checks passed
criccomini added a commit that referenced this pull request Feb 12, 2024
There was a bug in #423. Null types in a union with `extra_attrs` or other
attributes set would result in `False` returned when there was in fact a
NullType in the UnionType's `types` list. `__eq__` checks all attributes, so you
must iterate over the UnionType's `types` attribute and look for any type with
`isinstance(..., NullType) is True`. I updated the logic accordingly.

In doing so, I discovered that the JSON converter logic was converting JSON
schema fields of `null` type to a UnionType with a single nested NullType type.
This seems wrong; I updated the test to validate that `null` JSON fields are
returned as `NullType` with a default of `None`.
@criccomini
Copy link
Contributor

@adrianisk I've merged your PR and made the remaining tweaks here:

#426

Note that my PR results in a subtle change to the way null JSON schema types are handled. See the PR for details.

criccomini added a commit that referenced this pull request Feb 12, 2024
There was a bug in #423. Null types in a union with `extra_attrs` or other
attributes set would result in `False` returned when there was in fact a
NullType in the UnionType's `types` list. `__eq__` checks all attributes, so you
must iterate over the UnionType's `types` attribute and look for any type with
`isinstance(..., NullType) is True`. I updated the logic accordingly.

In doing so, I discovered that the JSON converter logic was converting JSON
schema fields of `null` type to a UnionType with a single nested NullType type.
This seems wrong; I updated the test to validate that `null` JSON fields are
returned as `NullType` with a default of `None`.
@criccomini
Copy link
Contributor

criccomini commented Feb 29, 2024

Released in 0.12.0:

https://pypi.org/project/recap-core/0.12.0/

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.

Handle lists in JSON schema parsing
2 participants