Skip to content

Allow lists being marked as httpPayload#1043

Merged
mtdowling merged 1 commit intosmithy-lang:mainfrom
timocov:allow-lists-as-payload
Mar 5, 2026
Merged

Allow lists being marked as httpPayload#1043
mtdowling merged 1 commit intosmithy-lang:mainfrom
timocov:allow-lists-as-payload

Conversation

@timocov
Copy link
Copy Markdown
Contributor

@timocov timocov commented Mar 3, 2026

Issue #, if available:

Description of changes:

When developing a custom protocol based on JSON it is not possible to have a list marked as @httpPayload, e.g. for the model:

operation Op {
  output := {
    @httpPayload
    content: StringList
  }
}

list StringList {
  member: String
}

Whilst it is possible to define such model in a custom protocol (with aws.protocols#restJson1 it still validates the model and fails with an error that AWS Protocols don't support it).

I understand why built-in/AWS protocols might not want to support it (e.g. enforcing best practices), but it feels like it should be possible to define this in a custom protocol (especially for backward compatibility when onboarding smithy to an existing services).

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@timocov timocov force-pushed the allow-lists-as-payload branch from b3d4dbf to 307141e Compare March 3, 2026 17:15
@mtdowling
Copy link
Copy Markdown
Member

Have you been able to actually define a model with an httpPayload trait that targets a list? Is this change bigger than this PR and would also require model changes?

@timocov
Copy link
Copy Markdown
Contributor Author

timocov commented Mar 5, 2026

Have you been able to actually define a model with an httpPayload trait that targets a list?

@mtdowling yep, and it works just fine. I can share more if you want (but tell me what you're interested in as it might be a bit too much 😂). And this was the only change I made in smithy-java to make it work. This is the protocol and the model I used for my client and it worked just fine:

Details

SimpleRestJsonClientProtocol.java:

public final class SimpleRestJsonClientProtocol extends HttpBindingClientProtocol<AwsEventFrame> {

    private final JsonCodec codec;
    private final HttpErrorDeserializer errorDeserializer;

    /**
     * Constructs a new SimpleRestJsonClientProtocol.
     *
     * @param service The service being called. This is used when finding the discriminator
     *                of documents that use relative shape IDs.
     */
    public SimpleRestJsonClientProtocol(ShapeId service) {
        super(SimpleRestJsonTrait.ID);

        this.codec = JsonCodec.builder()
                .useJsonName(true)
                .useTimestampFormat(true)
                .defaultNamespace(service.getNamespace())
                .build();

        this.errorDeserializer = HttpErrorDeserializer.builder()
                .codec(codec)
                .serviceId(service)
                .build();
    }

    @Override
    public <I extends SerializableStruct, O extends SerializableStruct> HttpRequest createRequest(
            ApiOperation<I, O> operation,
            I input,
            Context context,
            URI endpoint
    ) {
        RequestSerializer serializer = httpBinding().requestSerializer()
                .operation(operation)
                .payloadCodec(payloadCodec())
                .payloadMediaType(payloadMediaType())
                .shapeValue(input)
                .endpoint(endpoint)
                .omitEmptyPayload(omitEmptyPayload())
                .allowEmptyStructPayload(hasStructPayload(input));

        if (operation instanceof InputEventStreamingApiOperation<?, ?, ?> i) {
            serializer.eventEncoderFactory(getEventEncoderFactory(i));
        }

        return serializer.serializeRequest();
    }

    @Override
    public Codec payloadCodec() {
        return codec;
    }

    @Override
    protected String payloadMediaType() {
        return "application/json";
    }

    @Override
    protected HttpErrorDeserializer getErrorDeserializer(Context context) {
        return errorDeserializer;
    }

    @Override
    protected boolean omitEmptyPayload() {
        return true;
    }

    @Override
    protected EventEncoderFactory<AwsEventFrame> getEventEncoderFactory(
            InputEventStreamingApiOperation<?, ?, ?> inputOperation
    ) {
        // Event streaming is not currently supported for simpleRestJson protocol
        throw new UnsupportedOperationException("Event streaming is not yet supported for simpleRestJson protocol");
    }

    /**
     * Checks if the input has a struct payload member.
     */
    private <I extends SerializableStruct> boolean hasStructPayload(I input) {
        var members = input.schema().members();
        for (var member : members) {
            if (member.type().equals(ShapeType.STRUCTURE)
                    && member.hasTrait(TraitKey.HTTP_PAYLOAD_TRAIT)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Factory for creating SimpleRestJsonClientProtocol instances.
     */
    public static final class Factory implements ClientProtocolFactory<SimpleRestJsonTrait> {
        @Override
        public ShapeId id() {
            return SimpleRestJsonTrait.ID;
        }

        @Override
        public ClientProtocol<?, ?> createProtocol(ProtocolSettings settings, SimpleRestJsonTrait trait) {
            return new SimpleRestJsonClientProtocol(settings.service());
        }
    }
}

main.smithy:

@cors
@simpleRestJson
service ServiceName {
    operations: [
        GetObjects
    ]
}

@http(uri: "/v1/path", method: "GET")
operation GetObjects {
    output := {
        @httpPayload
        items: ObjectList
    }
}

list ObjectList {
    member: Object
}

structure Object {
    id: Integer
}

@timocov
Copy link
Copy Markdown
Contributor Author

timocov commented Mar 5, 2026

Is this change bigger than this PR and would also require model changes?

In theory it would be possible to use with aws restJson1 protocol, but it has a model validation for this so I didn't investigate it further to keep it consistent with current behavior.

@mtdowling
Copy link
Copy Markdown
Member

Cool. Seems fine then. Can you push up a rebase, and then I'll merge?

@timocov timocov force-pushed the allow-lists-as-payload branch from 307141e to 2c933f5 Compare March 5, 2026 18:35
@timocov
Copy link
Copy Markdown
Contributor Author

timocov commented Mar 5, 2026

@mtdowling done

@mtdowling mtdowling merged commit a79639d into smithy-lang:main Mar 5, 2026
2 checks passed
@mtdowling
Copy link
Copy Markdown
Member

Thanks!

@timocov timocov deleted the allow-lists-as-payload branch March 5, 2026 23:45
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