diff --git a/lib_json/src/main/x/json/annotations/JsonProperty.x b/lib_json/src/main/x/json/annotations/JsonProperty.x new file mode 100644 index 0000000000..6e76087372 --- /dev/null +++ b/lib_json/src/main/x/json/annotations/JsonProperty.x @@ -0,0 +1,17 @@ +/** + * An annotation to allow customization of the JSON name of a value. + * + * For example, if the JSON field name for a property needs to be "$data", this is invalid as an + * Ecstasy identifier name. The property can be annotated like this: + * + * @JsonProperty("$data") + * String data; + * + * The [ReflectionMapping] will use the value from the annotation to map the JSON field name to the + * property name. + * + * @param jsonName - the customized name of the field + */ +annotation JsonProperty(String jsonName) + into Class | Property { +} diff --git a/lib_json/src/main/x/json/mappings/ReflectionMapping.x b/lib_json/src/main/x/json/mappings/ReflectionMapping.x index 7840003083..de0b5bd4f0 100644 --- a/lib_json/src/main/x/json/mappings/ReflectionMapping.x +++ b/lib_json/src/main/x/json/mappings/ReflectionMapping.x @@ -1,3 +1,5 @@ +import annotations.JsonProperty; + /** * A reflection-based [Mapping] implementation that works for exactly one type, `Serializable`. */ @@ -129,15 +131,30 @@ const ReflectionMapping( val structType = clazz.StructType; PropertyMapping[] fields = new PropertyMapping[]; + Set names = new HashSet(); for (Property prop : structType.properties) { if (prop.hasField && !prop.lazy) { assert !prop.isConstant() && !prop.abstract && !prop.injected; // TODO CP what if the referent type is the same as "type"? (linked list example) if (Mapping valueMapping := schema.findMapping(prop.Referent)) { - // TODO CP - name has to be unique + String name = prop.name; + if (prop.is(JsonProperty)) { + name = prop.jsonName; + } + if (names.contains(name)) { + for (PropertyMapping mapping : fields) { + if (mapping.name == name) { + throw new IllegalJSON($|Duplicate JSON property name {name} \ + | {prop} name matches {mapping.property} + ); + } + } + } + names.add(name); + fields += new PropertyMapping - (prop.name, valueMapping, prop); + (name, valueMapping, prop); } } } diff --git a/lib_json/src/test/x/JsonTest/annotations/JsonPropertyTest.x b/lib_json/src/test/x/JsonTest/annotations/JsonPropertyTest.x new file mode 100644 index 0000000000..a1b1b90f52 --- /dev/null +++ b/lib_json/src/test/x/JsonTest/annotations/JsonPropertyTest.x @@ -0,0 +1,34 @@ +import json.Mapping; +import json.ObjectInputStream; +import json.Parser; +import json.Schema; + +import json.annotations.JsonProperty; + +class JsonPropertyTest { + + Schema jsonSchema = Schema.DEFAULT; + + static const Data(@JsonProperty("$data") String? s, String foo) {} + + @Test + void shouldParseValueWithCustomPropertyName() { + Data data = new Data("One", "Two"); + String jsonStr = \|{"$data":"One", "foo":"Two"} + ; + Mapping valueMapping = jsonSchema.ensureMapping(Data); + using (ObjectInputStream stream = new ObjectInputStream(jsonSchema, jsonStr.toReader())) { + val parsed = valueMapping.read(stream.ensureElementInput()); + assert parsed == data; + } + } + + @Test + public void shouldWriteValueWithCustomPropertyName() { + StringBuffer buf = new StringBuffer(); + Data data = new Data("One", "Two"); + jsonSchema.createObjectOutput(buf).write(data); + assert buf.toString() == \|{"$data":"One","foo":"Two"} + ; + } +}