Skip to content

Commit

Permalink
Merge pull request #180 from benoitlouy/jsonunknown-trait
Browse files Browse the repository at this point in the history
Define jsonUnknown trait
  • Loading branch information
lewisjkl authored Aug 13, 2024
2 parents 69967f6 + c3170e1 commit 5cc8125
Show file tree
Hide file tree
Showing 8 changed files with 211 additions and 1 deletion.
45 changes: 44 additions & 1 deletion docs/serialisation/json.md
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ The JSON objects
{}
```

are respectively decoded as follows in Scala (when using [smithy4s](https://disneystreaming.github.io/smithy4s/)):
are respectively decoded as follows in Scala (when using [smithy4s](https://disneystreaming.github.io/smithy4s/)):

```scala
Foo(Some(Nullable.Null), None)
Expand All @@ -152,3 +152,46 @@ or some similar type which preserves the information that an explicit `null` was
```

This means that `@nullable` allows round-tripping null values.


#### Unknown fields

Retaining JSON fields whose label do not match structure member names is supported via the `@jsonUnknown` smithy trait. This trait can be applied to a single structure member targeting a `map` with `Document` values.

JSON decoders supporting this trait must store unknown properties in the annotated map. Symmetrically, JSON encoders must inline the values from the map in the JSON object produced when serializing the enclosing structure.

Note that if a JSON document contains a field using the same label as the member annotated with the `@jsonUnknown` trait, it will be treated as an unknown field.

For example, given the following smithy definitions

```smithy
use alloy#jsonUnknown
structure Data {
known: String
@jsonUnknown
unknown: UnknownProperties
}
map UnknownProperties {
key: String
value: Document
}
```

The JSON objects

```json
{ "known": "known value" }
{ "known": "known value", "aField": 1, "anotherField": "another value" }
{ "known": "known value", "unknown": 1 }
```

are respectively decoded as follows in Scala (when using [smithy4s](https://disneystreaming.github.io/smithy4s/))

```scala
Data(known=Some("known value"), unknown=None)
Data(known=Some("known value"),
unknown=Some(Map("aField" -> Document.DNunmber(1), "anotherField" -> Document.DString("another value"))))
Data(known=Some("known value"), unknown=Some(Map("unknown" -> Document.DNumber(1))))
```
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ alloy.DataExamplesTrait$Provider
alloy.DateFormatTrait$Provider
alloy.DefaultValueTrait$Provider
alloy.DiscriminatedUnionTrait$Provider
alloy.JsonUnknownTrait$Provider
alloy.OpenEnumTrait$Provider
alloy.NullableTrait$Provider
alloy.openapi.OpenApiExtensionsTrait$Provider
Expand Down
10 changes: 10 additions & 0 deletions modules/core/resources/META-INF/smithy/jsonunknown.smithy
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
$version: "2"

namespace alloy

/// Retain unknown fields of a containing structure in this map member
@trait(
selector: "structure > member :test(> map > member > document)"
structurallyExclusive: "member"
)
structure jsonUnknown {}
1 change: 1 addition & 0 deletions modules/core/resources/META-INF/smithy/manifest
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ presence.smithy
proto/proto.smithy
restjson.smithy
string.smithy
jsonunknown.smithy
unions.smithy
urlform.smithy
uuid.smithy
Expand Down
1 change: 1 addition & 0 deletions modules/core/resources/META-INF/smithy/restjson.smithy
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ namespace alloy
alloy#discriminated
alloy#nullable
alloy#untagged
alloy#jsonUnknown
]
)
@trait(selector: "service")
Expand Down
34 changes: 34 additions & 0 deletions modules/core/src/alloy/JsonUnknownTrait.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/* Copyright 2022 Disney Streaming
*
* Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://disneystreaming.github.io/TOST-1.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package alloy;

import software.amazon.smithy.model.node.Node;
import software.amazon.smithy.model.shapes.ShapeId;
import software.amazon.smithy.model.traits.AnnotationTrait;

public final class JsonUnknownTrait extends AnnotationTrait {
public static ShapeId ID = ShapeId.from("alloy#jsonUnknown");

public JsonUnknownTrait() {
super(ID, Node.objectNode());
}

public static final class Provider extends AnnotationTrait.Provider<JsonUnknownTrait> {
public Provider() {
super(ID, (node) -> new JsonUnknownTrait());
}
}
}
13 changes: 13 additions & 0 deletions modules/core/test/resources/META-INF/smithy/traits.smithy
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use alloy#openEnum
use alloy#simpleRestJson
use alloy#structurePattern
use alloy#uncheckedExamples
use alloy#jsonUnknown
use alloy#untagged
use alloy#urlFormFlattened
use alloy#urlFormName
Expand Down Expand Up @@ -211,3 +212,15 @@ structure TestUrlFormName {
@urlFormName("Test")
test: String
}

structure TestJsonUnknown {
foo: String
bar: String
@jsonUnknown
bazes: UnknownProps
}

map UnknownProps {
key: String
value: Document
}
107 changes: 107 additions & 0 deletions modules/core/test/src/alloy/JsonUnknownTraitProviderSpec.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
/* Copyright 2022 Disney Streaming
*
* Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://disneystreaming.github.io/TOST-1.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package alloy

import software.amazon.smithy.model.Model
import software.amazon.smithy.model.shapes.ShapeId
import software.amazon.smithy.model.validation.Severity

import java.util.Optional
import scala.jdk.CollectionConverters.*
import scala.jdk.OptionConverters.*

final class JsonUnknownTraitProviderSpec extends munit.FunSuite {

test("has trait") {
val source =
"""|$version: "2"
|
|namespace test
|
|use alloy#jsonUnknown
|
|map UnknownProps {
| key: String
| value: Document
|}
|
|structure MyStruct {
| @jsonUnknown
| myMap: UnknownProps
|}
|""".stripMargin

val model =
Model.assembler
.discoverModels()
.addUnparsedModel("/test.smithy", source)
.assemble()
.unwrap()

val result = model
.getShape(ShapeId.from("test#MyStruct$myMap"))
.map(shape => shape.hasTrait(classOf[JsonUnknownTrait]))

assertEquals(result, Optional.of(true))
}

test("trait can only be applied to a single member") {
val source =
"""|$version: "2"
|
|namespace test
|
|use alloy#jsonUnknown
|
|map UnknownProps {
| key: String
| value: Document
|}
|
|structure MyStruct {
| @jsonUnknown
| first: UnknownProps
| @jsonUnknown
| second: UnknownProps
|}
|""".stripMargin

val result =
Model.assembler
.discoverModels()
.addUnparsedModel("/test.smithy", source)
.assemble()

assert(result.isBroken())

val errors = result
.getValidationEvents()
.asScala
.filter(ev => ev.getSeverity() == Severity.ERROR)
.toList

assertEquals(errors.length, 1)

val List(theError) = errors

assertEquals(
Some(ShapeId.from("test#MyStruct")),
theError.getShapeId().toScala
)
assertEquals(theError.getId(), "ExclusiveStructureMemberTrait")

}
}

0 comments on commit 5cc8125

Please sign in to comment.