Skip to content

About

Merge two OpenAPI objects into one that covers both, reconciling schemas inferred from independent samples of real data. Handles partial evidence such as values observed only as null, integer and number widening, and dates encoded either as strings or as timestamps.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

152 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Go Reference Code Coverage License

A gopher clicking two incomplete jigsaw pieces together into one complete piece

Two views of the same endpoint, reconciled.

Code Coverage

openapi-merge combines two OpenAPI 3.x objects into one that covers both. It exists for the problem of incomplete evidence: when a schema is inferred from a sample of real data, each sample tells you only part of the story, and the parts have to be reconciled.

Introduction

Observe GET /users/{id} once and you might see {"id": 1, "name": "Alice"}. Observe it again and you get {"id": 2, "name": "Bob", "nickname": null}. Neither response is the schema. The schema is what you get by merging them: three properties, one of them optional, one of them only ever seen as null.

That is what this module does. It is used by openapi-enrich, which builds specifications from recorded HTTP traffic and calls in here every time a second observation of the same endpoint arrives.

Merging is destructive and asymmetric by design: b is merged into a, in place. If the two cannot be reconciled, an error is returned describing the exact JSON path at which they conflict.

Features

Beyond combining properties and widening optionality, the merge handles the particular ways that sample-derived schemas disagree:

  • Required — an object requires only what both sides require: a value without a member makes it optional, as a recorded sample requires every member it has. What an inline allOf part requires is narrowed the same way.
  • Null — a value observed only as null has the type null. Merged with a real type, the result is that type, made nullable (["string", "null"]), rather than a conflict.
  • Arrays only ever seen empty — {"type": "array", "maxItems": 0} says nothing about the items, so the other side's items are adopted, and item bounds widen to cover both sides.
  • Tuples — a tuple (prefixItems) merges position by position with a tuple of its length. A sample keeps no length, so a list whose item type fits every position, such as [x, y, z] recorded as a list of numbers, merges into each position too. Any other list or tuple becomes an alternative beside it in a oneOf, and later samples go to the alternative of their own shape, or add one.
  • Numeric widening — an integer in one sample and a floating-point number in another merge to a number.
  • Dates in two encodings — a value seen as a date-time string in one sample and as a Unix timestamp integer in another becomes a oneOf of the two, rather than one silently discarding the other.
  • Dates with and without a time — a value seen as a date in one sample and as a date-time in another, as Notion's date start is, becomes a oneOf of the two.
  • Union routing — when one side already covers several shapes, with oneOf or anyOf, the other is merged into whichever branch it matches. Of a tagged union's objects, the branch is the one whose pinned properties — a const or one-value enum, such as "type": {"const": "select"} — the sample has, so a sample never lands in a sibling's branch; of several that match, it is the one that declares the most of the sample's properties, so a full object wins over its partial form. Of branches that pin nothing, it is likewise the one that declares the most of the sample's properties, and, of those that declare as many, one whose required properties the sample has. With none that matches, the merge fails and names the values it got. A branch that is itself a union matches as its own branches do, an integer matches a number branch, and a string without a format matches when no branch has the sample's.
  • Samples — a union marked x-samples (see Samples) holds samples of one value, such as the elements of a recorded array, not alternatives the value may take. Merged into a union, each sample goes into the branch it matches, so the elements of a list of mixed variants each reach their own; merged into a schema that is no union, each is merged into it in turn; merged into another union of samples, they join it, for the caller to collapse. Unlike examples, which hold values and stay in the specification, it holds schemas inferred from values and is only a working marker between the inference and the merge: openapi-enrich routes or collapses every one, so none reaches a finished specification.
  • Common properties beside a union — an allOf of objects and one union takes each sampled property into the part that declares it, and the rest into the branch the whole sample matches.
  • Scalar-or-array parameters — a parameter that appeared as a bare value in one sample and as an array in another merges the value into the array's item schema.
  • Enums — an example value not yet present in an enum is added to it.

Usage

go get github.com/MarkRosemaker/openapi-merge
import (
    "github.com/MarkRosemaker/openapi"
    merge "github.com/MarkRosemaker/openapi-merge"
)

// b is merged into a; a is modified in place.
if err := merge.Schema(a, b, false); err != nil {
    log.Fatal(err) // e.g. properties["age"].type: "string" != "integer"
}

The final argument to Schema marks whether the schemas describe a parameter, which enables the scalar-or-array reconciliation above — that mismatch is an artifact of how query parameters get sampled, and applying it to a request body would mask a genuine conflict.

Merging is available for each object kind:

Function Merges
merge.Schema(a, b *openapi.Schema, isParam bool) Two schemas
merge.SchemaRefs(a *openapi.SchemaRefs, b openapi.SchemaRefs) Two sets of named schemas
merge.Parameter(a, b *openapi.Parameter) Two parameters
merge.Response(a, b *openapi.Response) Two responses
merge.MediaType(a, b *openapi.MediaType) Two media types
merge.Content(a *openapi.Content, b openapi.Content) Two content maps

Errors carry the full JSON path to the conflict, via errpath.

The openapi family

Module Purpose
openapi Parse, validate, and write OpenAPI 3.x specifications
openapi-compare Compare specification objects — exact equality and shape equivalence
openapi-edit Safe structural edits, such as renaming a schema and rewriting every $ref to it
openapi-flatten Promote inline definitions into named components entries
openapi-compress Deduplicate and merge equivalent component schemas
openapi-merge (this module) Merge schemas that were inferred independently from different samples
openapi-enrich Infer specification content from observed HTTP traffic
openapi-codegen Generate Go types, clients, and servers from a specification

Two neighbours are easy to confuse with this one:

  • openapi-compare reports on two objects without changing them. This module changes them.
  • openapi-compress merges schemas too, but for a different reason — it collapses redundancy within a single finished document, whereas this module reconciles partial evidence about the same thing.

Additional Information

Contributing

Contributions are welcome — please open an issue or a pull request on GitHub.

License

This project is licensed under the Apache 2.0 License.

About

Merge two OpenAPI objects into one that covers both, reconciling schemas inferred from independent samples of real data. Handles partial evidence such as values observed only as null, integer and number widening, and dates encoded either as strings or as timestamps.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages