Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@fzyukio
Copy link
Contributor

@fzyukio fzyukio commented Jun 26, 2025

Issue:

When using MapOf in Goa designs, depending on the type of they key, we can have different results:

  • If the key is String, then the value is properly typed (via additionalProperties + $ref annotation)
  • Otherwise, the value is always free-form dict (additionalProperties: true)

Cause:

Goa generates OpenAPI 3 specs according to https://swagger.io/docs/specification/v3_0/data-models/dictionaries/, which states that

OpenAPI lets you define dictionaries where the keys are strings

However, the condition (keys are strings) are implicit and cannot be changed any way (RFC 8259). So no matter what KeyType is in MapOf(KeyType, ...), the results are the same, and any reader of the generated specs will interpret the map as having String key.

So by using additionalProperties: true we lose information that would otherwise be useful for readers of the specs (for example, oapi-codegen wouldn't be able to properly generate the correct type).

Solution:

  • Remove the restrictive condition check for KeyType == String

Example:

Design

// Design
package design

import . "goa.design/goa/v3/dsl"

var _ = API("map-demo", func() {
	Title("Map Types Demonstration API")
	Description("Simple API demonstrating various map types")
	Version("1.0")
})

// Simple data object for demonstration
var DataObject = Type("DataObject", func() {
	Attribute("id", String, "Object ID")
	Attribute("value", String, "Object value")
	Required("id", "value")
})

var _ = Service("maps", func() {
	Description("Service demonstrating map types")

	Method("int-to-object", func() {
		Description("Returns map from integer to object")
		Result(MapOf(Int, DataObject))
		HTTP(func() {
			GET("/int-to-object")
			Response(StatusOK)
		})
	})

	Method("string-to-object", func() {
		Description("Returns map from string to object")
		Result(MapOf(String, DataObject))
		HTTP(func() {
			GET("/string-to-object")
			Response(StatusOK)
		})
	})

	Method("int-to-array", func() {
		Description("Returns map from integer to array of objects")
		Result(MapOf(Int, ArrayOf(DataObject)))
		HTTP(func() {
			GET("/int-to-array")
			Response(StatusOK)
		})
	})

	Method("string-to-array", func() {
		Description("Returns map from string to array of objects")
		Result(MapOf(String, ArrayOf(DataObject)))
		HTTP(func() {
			GET("/string-to-array")
			Response(StatusOK)
		})
	})
})

Current behaviour (as of v3.21.1)

openapi: 3.0.3
info:
    title: Map Types Demonstration API
    description: Simple API demonstrating various map types
    version: "1.0"
servers:
    - url: http://localhost:80
      description: Default server for map-demo
paths:
    /int-to-array:
        get:
            tags:
                - maps
            summary: int-to-array maps
            description: Returns map from integer to array of objects
            operationId: maps#int-to-array
            responses:
                "200":
                    description: OK response.
                    content:
                        application/json:
                            schema:
                                type: object
                                additionalProperties: true <-- (1)
    /int-to-object:
        get:
            tags:
                - maps
            summary: int-to-object maps
            description: Returns map from integer to object
            operationId: maps#int-to-object
            responses:
                "200":
                    description: OK response.
                    content:
                        application/json:
                            schema:
                                type: object
                                additionalProperties: true  <-- (2)
    /string-to-array:
        get:
            tags:
                - maps
            summary: string-to-array maps
            description: Returns map from string to array of objects
            operationId: maps#string-to-array
            responses:
                "200":
                    description: OK response.
                    content:
                        application/json:
                            schema:
                                type: object
                                additionalProperties:
                                    type: array
                                    items:
                                        $ref: '#/components/schemas/DataObject' <-- (3)
    /string-to-object:
        get:
            tags:
                - maps
            summary: string-to-object maps
            description: Returns map from string to object
            operationId: maps#string-to-object
            responses:
                "200":
                    description: OK response.
                    content:
                        application/json:
                            schema:
                                type: object
                                additionalProperties:
                                    $ref: '#/components/schemas/DataObject' <-- (4)
components:
    schemas:
        DataObject:
            type: object
            properties:
                id:
                    type: string
                    description: Object ID
                value:
                    type: string
                    description: Object value
            example:
                id: Voluptatem aut accusantium.
                value: Quo illo.
            required:
                - id
                - value
tags:
    - name: maps
      description: Service demonstrating map types

New behaviour:

openapi: 3.0.3
info:
    title: Map Types Demonstration API
    description: Simple API demonstrating various map types
    version: "1.0"
servers:
    - url: http://localhost:80
      description: Default server for map-demo
paths:
    /int-to-array:
        get:
            tags:
                - maps
            summary: int-to-array maps
            description: Returns map from integer to array of objects
            operationId: maps#int-to-array
            responses:
                "200":
                    description: OK response.
                    content:
                        application/json:
                            schema:
                                type: object
                                additionalProperties:
                                    type: array
                                    items:
                                        $ref: '#/components/schemas/DataObject' <-- (1)
    /int-to-object:
        get:
            tags:
                - maps
            summary: int-to-object maps
            description: Returns map from integer to object
            operationId: maps#int-to-object
            responses:
                "200":
                    description: OK response.
                    content:
                        application/json:
                            schema:
                                type: object
                                additionalProperties:
                                    $ref: '#/components/schemas/DataObject' <-- (2)
    /string-to-array:
        get:
            tags:
                - maps
            summary: string-to-array maps
            description: Returns map from string to array of objects
            operationId: maps#string-to-array
            responses:
                "200":
                    description: OK response.
                    content:
                        application/json:
                            schema:
                                type: object
                                additionalProperties:
                                    type: array
                                    items:
                                        $ref: '#/components/schemas/DataObject' <-- (3)
    /string-to-object:
        get:
            tags:
                - maps
            summary: string-to-object maps
            description: Returns map from string to object
            operationId: maps#string-to-object
            responses:
                "200":
                    description: OK response.
                    content:
                        application/json:
                            schema:
                                type: object
                                additionalProperties:
                                    $ref: '#/components/schemas/DataObject' <-- (4)
components:
    schemas:
        DataObject:
            type: object
            properties:
                id:
                    type: string
                    description: Object ID
                value:
                    type: string
                    description: Object value
            example:
                id: Voluptatem aut accusantium.
                value: Quo illo.
            required:
                - id
                - value
tags:
    - name: maps
      description: Service demonstrating map types

Compare:

(1): current = free-form, new = fully typed
(2): current = free-form, new = fully typed
(3): current = fully-typed, new = fully typed (same behaviour, because keys are strings)
(4): current = fully-typed, new = fully typed (same behaviour, because keys are strings)

@fzyukio fzyukio force-pushed the allow-mapof-non-string branch from 767819f to 1a135fc Compare June 26, 2025 03:58
@raphael
Copy link
Member

raphael commented Jun 26, 2025

This makes sense, thank you for the PR! There seems to be an issue with the tests (https://github.com/goadesign/goa/actions/runs/15892630093/job/44863034090?pr=3732), I'll be happy to merge once this is fixed. Running make locally can help finding issues faster.

@fzyukio fzyukio force-pushed the allow-mapof-non-string branch from 1a135fc to d840363 Compare June 26, 2025 22:50
@fzyukio fzyukio force-pushed the allow-mapof-non-string branch from d840363 to f0a6407 Compare June 26, 2025 22:53
@fzyukio
Copy link
Contributor Author

fzyukio commented Jun 27, 2025

This makes sense, thank you for the PR! There seems to be an issue with the tests (https://github.com/goadesign/goa/actions/runs/15892630093/job/44863034090?pr=3732), I'll be happy to merge once this is fixed. Running make locally can help finding issues faster.

Hi @raphael, thanks for your quick response.
I've fixed the issue, and make test passed.
I'm waiting your approval for the actions to complete. Cheers

@raphael raphael enabled auto-merge (squash) June 27, 2025 02:55
@raphael raphael disabled auto-merge June 27, 2025 02:55
@raphael
Copy link
Member

raphael commented Jun 27, 2025

Great, thank you!

@raphael raphael merged commit ea846a7 into goadesign:v3 Jun 27, 2025
9 checks passed
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