Skip to content

Commit

Permalink
Add support for json schema oneOf enumStrategy (#2504)
Browse files Browse the repository at this point in the history
This adds support for a new json schema setting, `enumStrategy`, that allows for variants
`enum` (current behaviour, and default) and `oneOf` (gets converted to a oneOf with const
string values, allowing for documentation of individual enum variants.)
  • Loading branch information
nated0g authored Feb 7, 2025
1 parent a26b950 commit 494ddef
Show file tree
Hide file tree
Showing 6 changed files with 179 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -85,11 +85,42 @@ public String toString() {
}
}

/**
* Configures how Smithy enum shapes are converted to JSON Schema
*/
public enum EnumStrategy {

/**
* Converts to a schema that uses enum, which contains an array of strings
*
* <p>This is the default setting used if not configured.
*/
ENUM("enum"),

/**
* Converts to a schema that uses oneOf, with an array of const strings and optional
* descriptions for documentation purposes
*/
ONE_OF("oneOf");

private final String stringValue;

EnumStrategy(String stringValue) {
this.stringValue = stringValue;
}

@Override
public String toString() {
return stringValue;
}
}

private boolean alphanumericOnlyRefs;
private boolean useJsonName;
private TimestampFormatTrait.Format defaultTimestampFormat = TimestampFormatTrait.Format.DATE_TIME;
private UnionStrategy unionStrategy = UnionStrategy.ONE_OF;
private MapStrategy mapStrategy = MapStrategy.PROPERTY_NAMES;
private EnumStrategy enumStrategy = EnumStrategy.ENUM;
private String definitionPointer;
private ObjectNode schemaDocumentExtensions = Node.objectNode();
private ObjectNode extensions = Node.objectNode();
Expand Down Expand Up @@ -186,6 +217,18 @@ public void setMapStrategy(MapStrategy mapStrategy) {
this.mapStrategy = mapStrategy;
}

public EnumStrategy getEnumStrategy() {
return enumStrategy;
}

/**
* Configures how Smithy enum shapes ae converted to JSON Schema
* @param enumStrategy The enum strategy to use
*/
public void setEnumStrategy(EnumStrategy enumStrategy) {
this.enumStrategy = enumStrategy;
}

public String getDefinitionPointer() {
return definitionPointer != null ? definitionPointer : jsonSchemaVersion.getDefaultDefinitionPointer();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,21 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.regex.Pattern;
import software.amazon.smithy.model.Model;
import software.amazon.smithy.model.node.Node.NonNumericFloat;
import software.amazon.smithy.model.node.StringNode;
import software.amazon.smithy.model.shapes.BigDecimalShape;
import software.amazon.smithy.model.shapes.BigIntegerShape;
import software.amazon.smithy.model.shapes.BlobShape;
import software.amazon.smithy.model.shapes.BooleanShape;
import software.amazon.smithy.model.shapes.ByteShape;
import software.amazon.smithy.model.shapes.DocumentShape;
import software.amazon.smithy.model.shapes.DoubleShape;
import software.amazon.smithy.model.shapes.EnumShape;
import software.amazon.smithy.model.shapes.FloatShape;
import software.amazon.smithy.model.shapes.IntegerShape;
import software.amazon.smithy.model.shapes.ListShape;
Expand Down Expand Up @@ -248,6 +251,43 @@ public Schema unionShape(UnionShape shape) {
}
}

@Override
public Schema enumShape(EnumShape shape) {
JsonSchemaConfig.EnumStrategy enumStrategy = converter.getConfig().getEnumStrategy();

switch (enumStrategy) {
case ENUM:
return super.enumShape(shape);
case ONE_OF:
Map<String, String> enumValues = shape.getEnumValues();
List<Schema> schemas = new ArrayList<>();

for (Map.Entry<String, MemberShape> entry : shape.getAllMembers().entrySet()) {
String memberName = entry.getKey();
MemberShape member = entry.getValue();
Schema enumSchema = Schema.builder()
.constValue(StringNode.from(enumValues.get(memberName)))
.description(member.getTrait(DocumentationTrait.class)
.map(DocumentationTrait::getValue)
.orElse(null))
.build();

schemas.add(enumSchema);
}

return buildSchema(shape,
Schema.builder()
.description(shape.getTrait(DocumentationTrait.class)
.map(DocumentationTrait::getValue)
.orElse(null))
.type("string")
.oneOf(schemas));
default: {
throw new SmithyJsonSchemaException(String.format("Unsupported enum strategy: %s", enumStrategy));
}
}
}

@Override
public Schema timestampShape(TimestampShape shape) {
return buildSchema(shape, createBuilder(shape, "string"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,43 @@ public void supportsIntEnumsByDefault() {
Node.assertEquals(document.toNode(), expected);
}

@Test
public void supportsDefaultEnumStrategy() {
Model model = Model.assembler()
.addImport(getClass().getResource("string-enums.smithy"))
.assemble()
.unwrap();

SchemaDocument document = JsonSchemaConverter.builder()
.model(model)
.build()
.convert();

Node expected = Node.parse(
IoUtils.toUtf8String(getClass().getResourceAsStream("string-enums.jsonschema.v07.json")));
Node.assertEquals(document.toNode(), expected);
}

@Test
public void supportsOneOfEnumStrategy() {
Model model = Model.assembler()
.addImport(getClass().getResource("string-enums.smithy"))
.assemble()
.unwrap();

JsonSchemaConfig config = new JsonSchemaConfig();
config.setEnumStrategy(JsonSchemaConfig.EnumStrategy.ONE_OF);
SchemaDocument document = JsonSchemaConverter.builder()
.model(model)
.config(config)
.build()
.convert();

Node expected = Node.parse(
IoUtils.toUtf8String(getClass().getResourceAsStream("string-enums-one-of.jsonschema.v07.json")));
Node.assertEquals(document.toNode(), expected);
}

@Test
public void intEnumsCanBeDisabled() {
Model model = Model.assembler()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"definitions": {
"Foo": {
"type": "object",
"properties": {
"bar": {
"$ref": "#/definitions/TestEnum"
}
}
},
"TestEnum": {
"type": "string",
"description": "This is a test enum",
"oneOf": [
{
"const": "Foo",
"description": "it really does foo"
},
{
"const": "Bar"
}
]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"definitions": {
"Foo": {
"type": "object",
"properties": {
"bar": {
"$ref": "#/definitions/TestEnum"
}
}
},
"TestEnum": {
"type": "string",
"enum": [
"Foo",
"Bar"
],
"description": "This is a test enum"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
$version: "2.0"

namespace smithy.example

structure Foo {
bar: TestEnum
}

@documentation("This is a test enum")
enum TestEnum {
@documentation("it really does foo")
FOO = "Foo"
BAR = "Bar"
}

0 comments on commit 494ddef

Please sign in to comment.