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

fix: update internal schema merging #665

Merged
merged 7 commits into from Jan 18, 2024
Merged

Conversation

ivan-tymoshenko
Copy link
Member

@ivan-tymoshenko ivan-tymoshenko commented Nov 25, 2023

This PR adds a proper schema merging for different cases:

  • root schema + oneOf/anyOf subschema
  • root schema + all allOf subschemas
  • root schema + then schema; root schema + else schema

Fix #648
Fix #642

index.js Outdated
}
Object.assign(mergedSchema.properties, allOfSchema.properties)
}
for (let location of locations) {
Copy link
Member

Choose a reason for hiding this comment

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

Won't a simple for loop be faster than iterators?

It was the case last time I checked

Copy link
Member Author

Choose a reason for hiding this comment

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

This kind of change doesn't create any perf difference unless it places in a very very high-loaded bottleneck (not this library). This library could be optimized by reducing the amount of time it clones the schemas.

Feel free to run benchmarks and prove me wrong.

Copy link
Member

Choose a reason for hiding this comment

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

I see, no problem I'll take your word 😄

index.js Outdated Show resolved Hide resolved
Copy link
Member

@mcollina mcollina left a comment

Choose a reason for hiding this comment

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

lgtm

@ivan-tymoshenko ivan-tymoshenko marked this pull request as draft November 25, 2023 14:42
index.js Outdated
}
for (const subSchema of Object.values(schema)) {
if (
typeof subSchema === 'object' &&
Copy link
Member

Choose a reason for hiding this comment

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

Not familiar so just asking, we are sure there can't be a null value right?

Suggested change
typeof subSchema === 'object' &&
subSchema != null &&

Copy link
Member Author

Choose a reason for hiding this comment

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

It shouldn't be, but this is a good catch. Will add a check.

@ivan-tymoshenko
Copy link
Member Author

Need a proper review for this one. This is more difficult than it might seem.

@ivan-tymoshenko ivan-tymoshenko changed the title fix: update merged schema local refs fix: update internal schema merging Dec 11, 2023
package.json Outdated
"ajv": "^8.10.0",
"ajv-formats": "^2.1.1",
"fast-deep-equal": "^3.1.3",
"fast-uri": "^2.1.0",
"rfdc": "^1.2.0",
"json-schema-ref-resolver": "^1.0.1"
"json-schema-ref-resolver": "^1.0.1",
"json-schema-merge-allof": "^0.8.1"
Copy link
Member

Choose a reason for hiding this comment

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

The use of https://www.npmjs.com/package/json-schema-merge-allof does not inspire me a lot of faith as it seems an unmaintained module. Is there an alternative? Can we fork or embed its code in here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Unfortunatelly this is the best solution I could find. We can write our own solution, but I don't have time for this right now. Anyway it should be better that our currect merge logic.

@ivan-tymoshenko ivan-tymoshenko marked this pull request as draft January 16, 2024 12:51
@ivan-tymoshenko
Copy link
Member Author

Waiting for https://github.com/fastify/merge-json-schemas release.

@ivan-tymoshenko ivan-tymoshenko marked this pull request as ready for review January 17, 2024 14:39

const schema = {
allOf: [
{ type: 'string' },
{ type: 'number' }
]
}
t.throws(() => build(schema), new Error('allOf schemas have different type values'))
try {
build(schema)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
build(schema)
build(schema)
t.fail('Should have thrown')


const schema = {
allOf: [
{ format: 'date' },
{ format: 'time' }
]
}
t.throws(() => build(schema), new Error('allOf schemas have different format values'))
try {
build(schema)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
build(schema)
build(schema)
t.fail('Should have thrown')

}
if (
schema.$id !== undefined &&
schema.$id.charAt(0) !== '#'
Copy link
Contributor

Choose a reason for hiding this comment

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

What if schema.$id is an empty string?

Copy link
Member Author

Choose a reason for hiding this comment

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

It shouldn't be an empty string. If user explicitly add a schema $id field with empty string value, it's kind of his problem. Anyway, this code will prefix all nested schemas' $refs with an empty string then.

mergeAllOfSchema(context, location, schema, clone(schema))
schema = location.schema
code += `
else throw new TypeError(\`The value of '${schemaRef}' does not match schema definition.\`)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
else throw new TypeError(\`The value of '${schemaRef}' does not match schema definition.\`)
else throw new TypeError(\`The value of '${schemaRef}' does not match the schema definition.\`)

?

Copy link
Member Author

Choose a reason for hiding this comment

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

I didn't add this error. If you want to update the error message for this error, you can create another PR.

Comment on lines 251 to +252
})
const noElseGreetingOutput = JSON.stringify({
kind: 'greeting',
foo: 'FOO',
bar: 42,
hi: 'HI',
hello: 45,
a: 'A',
b: 35
})
const noElseGreetingOutput = JSON.stringify({})
Copy link
Contributor

Choose a reason for hiding this comment

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

Why did you change this?

Copy link
Member Author

Choose a reason for hiding this comment

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

Because the test was wrong.

Here is a schema:

const noElseSchema = {

Here is an input data:

const greetingInput = {

As you can see the if condition doesn't fullfil as greetingInput.kind equal greeting and not a foobar, so it should use else schema instead of then. And as else schema is missing the result is an empty object.

})

test('allOf: throw Error if nullable mismatch /1', (t) => {
test('recursive nested allOfs', (t) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Why did you change these two allOf tests?

Copy link
Member Author

Choose a reason for hiding this comment

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

I didn't change the test here. I removed allOf: throw Error if nullable mismatch /1 test and added a couple of new tests. I dropped the allOf: throw Error if nullable mismatch /1 because now the merging algoritm is more accurate and can resolve the situation when two schemas has different nullable keyword values.

If one schema has nullable = true and another one has nullable = false the merged schema will have nullable = false. Now fast-json-stringify just throws an error in this case.

@Uzlopak
Copy link
Contributor

Uzlopak commented Jan 17, 2024

Tbh. This is a quite complex PR. I dont think that I could approve it with the full confidence that I understood everything. I read over it, and tbh. I did not make a deep dive. Just a raw review, if everything makes still kind of sense.

Also I dont want to stall to merge this, just because I am not confident enough to approve it.

For my approval I just need the confidence, that we didnt break the api contract and if we break it, it should be an informed decision.As we have a good code coverage in this project, it would enough for me, if no old tests were chaged, and if they were changed, that they dont reflect a significant behavioural change.

I would like to know why some of the tests got changed/removed in the allof.test.js, as it seems the only place where the behaviour changed significantly. If those questions are answered, I am happy to approve it.

@mcollina
Copy link
Member

@Uzlopak let me know if you have any further concerns, otherwise I'd like to ship this soon.

Copy link
Contributor

@Uzlopak Uzlopak left a comment

Choose a reason for hiding this comment

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

Thank you for the explaination.
LGTM

@mcollina mcollina merged commit d95ef87 into master Jan 18, 2024
21 checks passed
@Uzlopak Uzlopak deleted the fix-merge-schema-ref-resolving branch January 18, 2024 15:06
@Pezmc
Copy link

Pezmc commented Jan 30, 2024

@Uzlopak @mcollina I'm currently hitting this bug (oneOf results in a trailing comma) and can confirm that this PR resolves it. The latest release of fast-json-strinify was 5.10.0 three weeks ago and does not include this fix, how can I help cut a new release?

ivan-tymoshenko referenced this pull request Feb 1, 2024
Signed-off-by: Matteo Collina <hello@matteocollina.com>
mcollina added a commit that referenced this pull request Feb 1, 2024
ivan-tymoshenko added a commit that referenced this pull request Feb 4, 2024
mcollina pushed a commit that referenced this pull request Feb 5, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
5 participants