Skip to content

Dupplicate generated type with json-schema #9019

@dimitriBouteille

Description

@dimitriBouteille

Issue workflow progress

Progress of the issue based on the
Contributor Workflow

Make sure to fork this template and run yarn generate in the terminal.

Please make sure Mesh package versions under package.json matches yours.

  • 2. A failing test has been provided
  • 3. A local solution has been provided
  • 4. A pull request is pending review

Describe the bug

When I want to generate a schema from @omnigraph/json-schema, I have a type that is generated several times but with a different name, I can’t understand why and how the generation works.

Here is what my function loadJSONSchemaSubgraph looks like, nothing particular at this level:

loadJSONSchemaSubgraph('SuluRest', {
	endpoint: 'MY_ENDPOINT',
	operations: [
	  {
	    type: 'Query',
	    field: 'ambassadorList',
	    method: 'GET',
	    path: '/{context.headers.lang}/universe/ambassadors.json',
	    responseSchema:
	      './api/rest/ambassador.json#/definitions/AmbassadorList',
	  },
	],
})

Here is the structure of the ambassador.json file. This file uses several references that use the $ref to utils.json#/definitions/singleMediaSelection.json more than once :

ambassador.json
{
  "definitions": {
    "AmbassadorList": {
      "title": "AmbassadorList",
      "type": "object",
      "properties": {
        "content": {
          "title": "AmbassadorListContent",
          "type": "object",
          "properties": {
            "title": {
              "type": ["string", "null"]
            },
            "subtitle": {
              "type": ["string", "null"]
            },
            "image": {
              "$ref": "utils.json#/definitions/SingleMediaSelection"
            },
            "videoMobile": {
              "$ref": "utils.json#/definitions/singleMediaSelection"
            },
            "videoDesktop": {
              "$ref": "utils.json#/definitions/singleMediaSelection"
            },
            "autoplay": {
              "type": "boolean"
            },
            "videoLoop": {
              "type": "boolean"
            },
            "youtubeUrl": {
              "type": ["string", "null"]
            }
          }
        },
        "extension": {
          "$ref": "types/extension.json"
        }
      }
    }
  }
}

The types/extension.json also use utils.json :

{
  "type": "object",
  "title": "Extension",
  "properties": {
    "excerpt": {
      "$ref": "extension/excerpt.json"
    }
  }
}
extension/excerpt.json
{
  "title": "Excerpt",
  "type": "object",
  "properties": {
    "title": {
      "type": ["string", "null"]
    },
    "more": {
      "type": ["string", "null"]
    },
    "defaultMore": {
      "type": ["string", "null"]
    },
    "description": {
      "type": ["string", "null"]
    },
    "categories": {
      "$ref": "excerpt/categories.json"
    },
    "icon": {
      "type": "array",
      "items": {
        "$ref": "../../utils.json#/definitions/SingleMediaSelection"
      }
    },
    "images": {
      "type": "array",
      "items": {
        "$ref": "../../utils.json#/definitions/SingleMediaSelection"
      }
    },
    "tags": {
      "type": "array",
      "items": {
        "type": "string"
      }
    }
  }
}

The utils.json file is very simple :

{
  "definitions": {
    "SingleMediaSelection": {
      "$ref": "types/singleMediaSelection.json"
    }
  }
}

And the types/singleMediaSelection.json file is also very simple :

types/singleMediaSelection.json
{
  "title": "SingleMediaSelection",
  "type": "object",
  "properties": {
    "id": {
      "type": ["integer", "null"]
    },
    "locale": {
      "type": ["string", "null"]
    },
    "collection": {
      "type": ["integer", "null"]
    },
    "size": {
      "type": ["integer", "null"]
    },
    "mimeType": {
      "type": ["string", "null"]
    },
    "title": {
      "type": ["string", "null"]
    },
    "version": {
      "type": ["integer", "null"]
    },
    "subVersion": {
      "type": ["integer", "null"]
    },
    "name": {
      "type": ["string", "null"]
    },
    "description": {
      "type": ["string", "null"]
    },
    "type": {
      "type": ["object", "null"],
      "properties": {
        "name": {
          "type": ["string", "null"]
        },
        "id": {
          "type": ["integer", "null"]
        }
      }
    },
    "isImage": {
      "type": "boolean"
    },
    "isVideo": {
      "type": "boolean"
    },
    "isAudio": {
      "type": "boolean"
    },
    "isDocument": {
      "type": "boolean"
    },
    "publishLanguages": {
      "type": "array"
    },
    "contentLanguages": {
      "type": "array"
    },
    "tags": {
      "type": "array"
    },
    "url": {
      "type": ["string", "null"]
    },
    "changed": {
      "type": ["string", "null"]
    },
    "changer": {
      "type": ["string", "null"]
    },
    "created": {
      "type": ["string", "null"]
    },
    "creator": {
      "type": ["string", "null"]
    },
    "properties": {
      "type": ["object", "null"],
      "properties": {
        "width": {
          "type": ["integer", "null"]
        },
        "height": {
          "type": ["integer", "null"]
        }
      }
    },
    "categories": {
      "type": ["string", "null"]
    },
    "targetGroups": {
      "type": ["string", "null"]
    },
    "formatUri": {
      "type": "string"
    },
    "focusPointX": {
      "type": ["integer", "null"]
    },
    "focusPointY": {
      "type": ["integer", "null"]
    }
  }
}

When I excuse the following command npx mesh-compose -o . mesh/schema.graphql, the schema is indeed generated but the SingleMediaSelection type is duplicated several times with a different suffix: SingleMediaSelection, SingleMediaSelection1, SingleMediaSelection2, ... Here is the generated schema.graphql:

schema.graphql
schema
    @link(url: "https://specs.apollo.dev/link/v1.0")
    @link(url: "https://specs.apollo.dev/join/v0.3", for: EXECUTION)






    @link(
  url: "https://the-guild.dev/graphql/mesh/spec/v1.0"
  import: ["@httpOperation", "@transport", "@source", "@extraSchemaDefinitionDirective"]
)
  {
    query: Query


  }


    directive @join__enumValue(graph: join__Graph!) repeatable on ENUM_VALUE

    directive @join__graph(name: String!, url: String!) on ENUM_VALUE


      directive @join__field(
        graph: join__Graph
        requires: join__FieldSet
        provides: join__FieldSet
        type: String
        external: Boolean
        override: String
        usedOverridden: Boolean


      ) repeatable on FIELD_DEFINITION | INPUT_FIELD_DEFINITION



    directive @join__implements(
      graph: join__Graph!
      interface: String!
    ) repeatable on OBJECT | INTERFACE

    directive @join__type(
      graph: join__Graph!
      key: join__FieldSet
      extension: Boolean! = false
      resolvable: Boolean! = true
      isInterfaceObject: Boolean! = false
    ) repeatable on OBJECT | INTERFACE | UNION | ENUM | INPUT_OBJECT | SCALAR

    directive @join__unionMember(
      graph: join__Graph!
      member: String!
    ) repeatable on UNION

    scalar join__FieldSet



  directive @link(
    url: String
    as: String
    for: link__Purpose
    import: [link__Import]
  ) repeatable on SCHEMA

  scalar link__Import

  enum link__Purpose {
    """
    `SECURITY` features provide metadata necessary to securely resolve fields.
    """
    SECURITY

    """
    `EXECUTION` features provide metadata necessary for operation execution.
    """
    EXECUTION
  }








enum join__Graph {
  SULU_REST @join__graph(
    name: "SuluRest"
    url: "MY_ENDPOINT"
  )
}

directive @httpOperation(
  subgraph: String
  path: String
  operationSpecificHeaders: [[String]]
  httpMethod: HTTPMethod
  isBinary: Boolean
  requestBaseBody: ObjMap
  queryParamArgMap: ObjMap
  queryStringOptionsByParam: ObjMap
  jsonApiFields: Boolean
  queryStringOptions: ObjMap
) repeatable on FIELD_DEFINITION

directive @transport(
  subgraph: String
  kind: String
  location: String
  headers: [[String]]
  queryStringOptions: ObjMap
  queryParams: [[String]]
) repeatable on SCHEMA

directive @source(name: String!, type: String, subgraph: String!)  repeatable on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION

directive @extraSchemaDefinitionDirective(directives: _DirectiveExtensions)  repeatable on OBJECT

"""
The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf).
"""
scalar JSON @join__type(graph: SULU_REST)  @specifiedBy(
  url: "http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf"
)

scalar ObjMap @join__type(graph: SULU_REST)

scalar _DirectiveExtensions @join__type(graph: SULU_REST)

type Query @extraSchemaDefinitionDirective(
  directives: {transport: [{subgraph: "SuluRest", kind: "rest", location: "MY_ENDPOINT"}]}
) @join__type(graph: SULU_REST)  {
  ambassadorList: AmbassadorList @httpOperation(
    subgraph: "SuluRest"
    path: "/{context.headers.lang}/universe/ambassadors.json"
    httpMethod: GET
  )
}

type AmbassadorList @join__type(graph: SULU_REST)  {
  content: AmbassadorListContent
  extension: Extension
}

type AmbassadorListContent @join__type(graph: SULU_REST)  {
  title: String
  subtitle: String
  image: SingleMediaSelection
  videoMobile: JSON
  videoDesktop: JSON
  autoplay: Boolean
  videoLoop: Boolean
  youtubeUrl: String
}

type SingleMediaSelection @join__type(graph: SULU_REST)  {
  id: Int
  locale: String
  collection: Int
  size: Int
  mimeType: String
  title: String
  version: Int
  subVersion: Int
  name: String
  description: String
  type: query_ambassadorList_content_image_type
  isImage: Boolean
  isVideo: Boolean
  isAudio: Boolean
  isDocument: Boolean
  publishLanguages: [JSON]
  contentLanguages: [JSON]
  tags: [JSON]
  url: String
  changed: String
  changer: String
  created: String
  creator: String
  properties: query_ambassadorList_content_image
  categories: String
  targetGroups: String
  formatUri: String
  focusPointX: Int
  focusPointY: Int
}

type query_ambassadorList_content_image_type @join__type(graph: SULU_REST)  {
  name: String
  id: Int
}

type query_ambassadorList_content_image @join__type(graph: SULU_REST)  {
  width: Int
  height: Int
}

type Extension @join__type(graph: SULU_REST)  {
  seo: Seo
  excerpt: Excerpt
}

type Seo @join__type(graph: SULU_REST)  {
  title: String
  description: String
  keywords: String
  canonicalUrl: String
  noIndex: Boolean
  noFollow: Boolean
  hideInSitemap: Boolean
  published: [String]
}

type Excerpt @join__type(graph: SULU_REST)  {
  title: String
  more: String
  defaultMore: String
  description: String
  categories: [AmbassadorCategory]
  icon: [SingleMediaSelection3]
  images: [SingleMediaSelection3]
  tags: [String]
}

type AmbassadorCategory @join__type(graph: SULU_REST)  {
  locale: String
  id: Int
  name: String
  ghostLocale: String
  parentId: Int
  medias: [SingleMediaSelection2]
}

type SingleMediaSelection2 @join__type(graph: SULU_REST)  {
  id: Int
  locale: String
  collection: Int
  size: Int
  mimeType: String
  title: String
  version: Int
  subVersion: Int
  name: String
  description: String
  type: query_ambassadorList_extension_excerpt_categories_items_medias_items_type
  isImage: Boolean
  isVideo: Boolean
  isAudio: Boolean
  isDocument: Boolean
  publishLanguages: [JSON]
  contentLanguages: [JSON]
  tags: [JSON]
  url: String
  changed: String
  changer: String
  created: String
  creator: String
  properties: query_ambassadorList_extension_excerpt_categories_items_medias_items
  categories: String
  targetGroups: String
  formatUri: String
  focusPointX: Int
  focusPointY: Int
}

type query_ambassadorList_extension_excerpt_categories_items_medias_items_type @join__type(graph: SULU_REST)  {
  name: String
  id: Int
}

type query_ambassadorList_extension_excerpt_categories_items_medias_items @join__type(graph: SULU_REST)  {
  width: Int
  height: Int
}

type SingleMediaSelection3 @join__type(graph: SULU_REST)  {
  id: Int
  locale: String
  collection: Int
  size: Int
  mimeType: String
  title: String
  version: Int
  subVersion: Int
  name: String
  description: String
  type: query_ambassadorList_extension_excerpt_icon_items_type
  isImage: Boolean
  isVideo: Boolean
  isAudio: Boolean
  isDocument: Boolean
  publishLanguages: [JSON]
  contentLanguages: [JSON]
  tags: [JSON]
  url: String
  changed: String
  changer: String
  created: String
  creator: String
  properties: query_ambassadorList_extension_excerpt_icon_items
  categories: String
  targetGroups: String
  formatUri: String
  focusPointX: Int
  focusPointY: Int
}

type query_ambassadorList_extension_excerpt_icon_items_type @join__type(graph: SULU_REST)  {
  name: String
  id: Int
}

type query_ambassadorList_extension_excerpt_icon_items @join__type(graph: SULU_REST)  {
  width: Int
  height: Int
}

enum HTTPMethod @join__type(graph: SULU_REST)  {
  GET @join__enumValue(graph: SULU_REST)
  HEAD @join__enumValue(graph: SULU_REST)
  POST @join__enumValue(graph: SULU_REST)
  PUT @join__enumValue(graph: SULU_REST)
  DELETE @join__enumValue(graph: SULU_REST)
  CONNECT @join__enumValue(graph: SULU_REST)
  OPTIONS @join__enumValue(graph: SULU_REST)
  TRACE @join__enumValue(graph: SULU_REST)
  PATCH @join__enumValue(graph: SULU_REST)
}

To Reproduce Steps to reproduce the behavior:

Expected behavior

The SingleMediaSelection type must be generated only once since the $ref always refers to the same file the graphQL

Environment:

  • OS: Ubuntu 24
  • @graphql-mesh/compose-cli : 1.5.3
  • @omnigraph/json-schema : 0.109.17
  • NodeJS: v22.21.1

Additional context

I am trying to migrate from mesh v0 to v1, with v0 there was no problem, the SingleMediaSelection type was generated only once. With v1, the type is generated several times ...

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions