Repository: asyncapi/modelina (ts)

Issue:
Add helper functions to un/marshal data models for Java
#### Reason/Context
We need some helper functions to be able to marshal the data model instances to JSON and unmarshal JSON to instances of the data model.

#### Description
The reason we need to write this our self is that we add properties to data models which are technically not properties directly, an example here is `additionalProperties`, as it is rendered as a property in the data model, but when you want to convert an instance of the data model to bytes or JSON for a payload, it should not be used as is but unwrapped.

Pull request:
feat: add un/marshal JSON preset for Java
<!--   Thank you for your contribution. Before you submit the pull request:
1. Follow our contribution guidelines
2. Test your changes and attach their results to the pull request.
3. Update the relevant documentation.
-->

**Description**
- Added un/marshalling in Java inside `src/generators/java/presets/CommonPreset.ts`

**Related issue(s)**
<!-- If you refer to a particular issue, provide its number, othewise, remove this section.
For example, `Resolves #123`, `Fixes #43`, or `See also #33`. The `See also #33` option will not automatically close the issue after the PR merge. -->
Fixed #488 

Patch:
diff --git a/docs/languages/Java.md b/docs/languages/Java.md
index f6a8b06c..37f28444 100644
--- a/docs/languages/Java.md
+++ b/docs/languages/Java.md
@@ -57,3 +57,12 @@ Check out this [example for a live demonstration](../../examples/java-generate-j
 To generate Java data models with Jackson annotation using `JAVA_JACKSON_PRESET` option.
 
 Check out this [example for a live demonstration](../../examples/java-generate-jackson-annotation).
+
+## Include JSON marshaling and unmarshaling methods
+
+Sometimes you just want to convert your class to JSON without the use of annotations such as Jackson.
+
+Check out this [example for a live demonstration](../../examples/java-generate-marshalling).
+
+External dependencies
+- Requires [org.json package](https://search.maven.org/artifact/org.json/json/20211205/bundle) to work
diff --git a/examples/java-generate-equals/index.ts b/examples/java-generate-equals/index.ts
index 113ea8f4..ba80aed5 100644
--- a/examples/java-generate-equals/index.ts
+++ b/examples/java-generate-equals/index.ts
@@ -7,7 +7,8 @@ const generator = new JavaGenerator({
       options: {
         equal: true,
         hashCode: false,
-        classToString: false
+        classToString: false,
+        marshalling: false,
       }
     }
   ]
diff --git a/examples/java-generate-hashcode/index.ts b/examples/java-generate-hashcode/index.ts
index 38737ab3..9caabfd6 100644
--- a/examples/java-generate-hashcode/index.ts
+++ b/examples/java-generate-hashcode/index.ts
@@ -7,7 +7,8 @@ const generator = new JavaGenerator({
       options: {
         equal: false,
         hashCode: true,
-        classToString: false
+        classToString: false,
+        marshalling: false,
       }
     }
   ]
diff --git a/examples/java-generate-marshalling/README.md b/examples/java-generate-marshalling/README.md
new file mode 100644
index 00000000..204acfdf
--- /dev/null
+++ b/examples/java-generate-marshalling/README.md
@@ -0,0 +1,17 @@
+# Java Data Models with un/marshalling functionality
+
+A basic example of how to use the un/marshalling functionality of the java class.
+
+## How to run this example
+
+Run this example using:
+
+```sh
+npm i && npm run start
+```
+
+If you are on Windows, use the `start:windows` script instead:
+
+```sh
+npm i && npm run start:windows
+```
diff --git a/examples/java-generate-marshalling/__snapshots__/index.spec.ts.snap b/examples/java-generate-marshalling/__snapshots__/index.spec.ts.snap
new file mode 100644
index 00000000..6f1ab208
--- /dev/null
+++ b/examples/java-generate-marshalling/__snapshots__/index.spec.ts.snap
@@ -0,0 +1,29 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Should be able to generate ts data model with marshal und unmarshal functions and should log expected output to console 1`] = `
+Array [
+  "public class Root {
+  private Object email;
+
+  public Object getEmail() { return this.email; }
+  public void setEmail(Object email) { this.email = email; }
+
+  public String marshal() {
+    List<String> propList = new ArrayList();
+      if(this.email != null) {
+            propList.add(\\"email:\\"+this.email.toString());
+        }
+    return propList.stream().collect(Collectors.joining(\\",\\"));
+  }
+
+  public static Root unmarshal(String json) {
+    Root result = new Root();
+    JSONObject jsonObject = new JSONObject(json);
+      if(jsonObject.has(\\"email\\")) {
+            result.setEmail(jsonObject.getString(\\"email\\"));
+          }
+    return result;
+  }
+}",
+]
+`;
diff --git a/examples/java-generate-marshalling/index.spec.ts b/examples/java-generate-marshalling/index.spec.ts
new file mode 100644
index 00000000..28b2ada9
--- /dev/null
+++ b/examples/java-generate-marshalling/index.spec.ts
@@ -0,0 +1,14 @@
+const spy = jest.spyOn(global.console, 'log').mockImplementation(() => { return; });
+import {generate} from './index';
+
+describe('Should be able to generate ts data model with marshal und unmarshal functions', () => {
+  afterAll(() => {
+    jest.restoreAllMocks();
+  });
+  test('and should log expected output to console', async () => {
+    await generate();
+    //Generate is called 2x, so even though we expect 1 model, we double it
+    expect(spy.mock.calls.length).toEqual(2);
+    expect(spy.mock.calls[1]).toMatchSnapshot();
+  });
+});
diff --git a/examples/java-generate-marshalling/index.ts b/examples/java-generate-marshalling/index.ts
new file mode 100644
index 00000000..06d21f8a
--- /dev/null
+++ b/examples/java-generate-marshalling/index.ts
@@ -0,0 +1,35 @@
+import { JavaGenerator, JAVA_COMMON_PRESET } from '../../src';
+
+const generator = new JavaGenerator({ 
+  presets: [
+    {
+      preset: JAVA_COMMON_PRESET, 
+      options: {
+        equal: false,
+        hashCode: false,
+        classToString: false,
+        marshalling: true,
+      }
+    }
+  ]
+});
+
+const jsonSchemaDraft7 = {
+  $schema: 'http://json-schema.org/draft-07/schema#',
+  type: 'object',
+  additionalProperties: false,
+  properties: {
+    email: {
+      type: 'string',
+      format: 'email'
+    }
+  }
+};
+
+export async function generate() : Promise<void> {
+  const models = await generator.generate(jsonSchemaDraft7);
+  for (const model of models) {
+    console.log(model.result);
+  }
+}
+generate();
diff --git a/examples/java-generate-marshalling/package-lock.json b/examples/java-generate-marshalling/package-lock.json
new file mode 100644
index 00000000..4db0c613
--- /dev/null
+++ b/examples/java-generate-marshalling/package-lock.json
@@ -0,0 +1,10 @@
+{
+  "name": "java-interface",
+  "lockfileVersion": 2,
+  "requires": true,
+  "packages": {
+    "": {
+      "hasInstallScript": true
+    }
+  }
+}
diff --git a/examples/java-generate-marshalling/package.json b/examples/java-generate-marshalling/package.json
new file mode 100644
index 00000000..4388b2eb
--- /dev/null
+++ b/examples/java-generate-marshalling/package.json
@@ -0,0 +1,10 @@
+{
+  "config" : { "example_name" : "java-generate-marshalling" },
+  "scripts": {
+    "install": "cd ../.. && npm i",
+    "start": "../../node_modules/.bin/ts-node --cwd ../../ ./examples/$npm_package_config_example_name/index.ts",
+    "start:windows": "..\\..\\node_modules\\.bin\\ts-node --cwd ..\\..\\ .\\examples\\%npm_package_config_example_name%\\index.ts",
+    "test": "../../node_modules/.bin/jest --config=../../jest.config.js ./examples/$npm_package_config_example_name/index.spec.ts",
+    "test:windows": "..\\..\\node_modules\\.bin\\jest --config=..\\..\\jest.config.js examples/%npm_package_config_example_name%/index.spec.ts"
+  }
+}
diff --git a/examples/java-generate-tostring/index.ts b/examples/java-generate-tostring/index.ts
index 263e14b1..07ac5014 100644
--- a/examples/java-generate-tostring/index.ts
+++ b/examples/java-generate-tostring/index.ts
@@ -8,6 +8,7 @@ const generator = new JavaGenerator({
         equal: false,
         hashCode: false,
         classToString: true,
+        marshalling: false,
       },
     },
   ],
diff --git a/src/generators/java/presets/CommonPreset.ts b/src/generators/java/presets/CommonPreset.ts
index 79559892..0b21f33a 100644
--- a/src/generators/java/presets/CommonPreset.ts
+++ b/src/generators/java/presets/CommonPreset.ts
@@ -1,13 +1,15 @@
 import { JavaRenderer } from '../JavaRenderer';
 import { JavaPreset } from '../JavaPreset';
 
-import { getUniquePropertyName, DefaultPropertyNames } from '../../../helpers';
+import { getUniquePropertyName, DefaultPropertyNames, FormatHelpers } from '../../../helpers';
 import { CommonModel } from '../../../models';
+import {Logger} from '../../../utils/LoggingInterface';
 
 export interface JavaCommonPresetOptions {
   equal: boolean;
   hashCode: boolean;
   classToString: boolean;
+  marshalling: boolean;
 }
 
 /**
@@ -97,6 +99,76 @@ private String toIndentedString(Object o) {
 }`;
 } 
 
+function renderMarshalProperties(renderer: JavaRenderer, model:CommonModel) {
+  const properties = model.properties || {};
+  const propertyKeys = [...Object.keys(properties)];
+  const marshalProperties = propertyKeys.map(prop => {
+    const formattedPropertyName = renderer.nameProperty(prop,model);
+    const modelInstanceVariable = `this.${formattedPropertyName}`;
+    return `if(${modelInstanceVariable} != null) {
+        propList.add("${formattedPropertyName}:"+${modelInstanceVariable}.toString());
+    }`;
+  });
+  return marshalProperties.join('\n');
+}
+/**
+ * Render `marshal` function based on model's properties
+ */
+function renderMarshalling({ renderer, model }: {
+  renderer: JavaRenderer,
+  model: CommonModel,
+}): string {
+  return `public String marshal() {
+  List<String> propList = new ArrayList();
+  ${renderer.indent(renderMarshalProperties(renderer,model))}
+  return propList.stream().collect(Collectors.joining(","));
+}`;
+}
+
+function renderUnmarshalProperties(renderer: JavaRenderer, model:CommonModel) {
+  const properties = model.properties || {};
+  const propertyKeys = [...Object.keys(properties)];
+  const unmarshalProperties = propertyKeys.map(prop => {
+    const formattedPropertyName = renderer.nameProperty(prop,model);
+    const setterFunction = `set${formattedPropertyName.charAt(0).toUpperCase()}${formattedPropertyName.slice(1)}`;
+    const propModel = properties[String(prop)];
+    if (propModel.type === 'undefined') {
+      Logger.error(`Could not render unmarshal for property ${prop}`);
+      return; 
+    }
+    if (propModel.type === 'array') {
+      return `if(jsonObject.has("${formattedPropertyName}")) {
+        JSONArray jsonArray = jsonObject.getJSONArray("${formattedPropertyName}");
+        String[] ${formattedPropertyName} = new String[jsonArray.length()];
+        for(int i = 0; i < jsonArray.length(); i++) {
+          ${formattedPropertyName}[i] = jsonArray.getString(i);
+        }      
+        result.${setterFunction}(${formattedPropertyName});
+      }`;
+    }
+    const getType = `jsonObject.get${FormatHelpers.upperFirst(propModel.type?.toString() || '')}`;
+    return `if(jsonObject.has("${formattedPropertyName}")) {
+        result.${setterFunction}(${getType}("${formattedPropertyName}"));
+      }`;
+  });
+  return unmarshalProperties.join('\n');
+}
+/**
+ * Render `unmarshal` function based on model's properties
+ */
+function renderUnmarshalling({ renderer, model }: {
+  renderer: JavaRenderer,
+  model: CommonModel,
+}): string {
+  const formattedModelName = renderer.nameType(model.$id);
+  return `public static ${formattedModelName} unmarshal(String json) {
+  ${formattedModelName} result = new ${formattedModelName}();
+  JSONObject jsonObject = new JSONObject(json);
+  ${renderer.indent(renderUnmarshalProperties(renderer,model))}
+  return result;
+}`;
+}
+
 /**
  * Preset which adds `equal`, `hashCode`, `toString` functions to class. 
  * 
@@ -110,15 +182,26 @@ export const JAVA_COMMON_PRESET: JavaPreset = {
       const shouldContainEqual = options.equal === undefined || options.equal === true;
       const shouldContainHashCode = options.hashCode === undefined || options.hashCode === true;
       const shouldContainToString = options.classToString === undefined || options.classToString === true;
+      const shouldContainMarshal = options.marshalling === true;
 
       if (shouldContainEqual === true || shouldContainHashCode === true) {
         renderer.addDependency('import java.util.Objects;');
       }
 
+      if (shouldContainMarshal === true) {
+        renderer.addDependency('import java.util.stream;');
+        renderer.addDependency('import org.json.JSONObject;');
+        renderer.addDependency('import java.util.Map;');
+      }
+
       if (shouldContainEqual) {blocks.push(renderEqual({ renderer, model }));}
       if (shouldContainHashCode) {blocks.push(renderHashCode({ renderer, model }));}
       if (shouldContainToString) {blocks.push(renderToString({ renderer, model }));}
-
+      
+      if (shouldContainMarshal === true) {
+        blocks.push(renderMarshalling({ renderer, model }));
+        blocks.push(renderUnmarshalling({ renderer, model }));
+      }
       return renderer.renderBlock([content, ...blocks], 2);
     },
   }


Test patch:
diff --git a/test/generators/java/presets/CommonPreset.spec.ts b/test/generators/java/presets/CommonPreset.spec.ts
index 62529a49..9052bb87 100644
--- a/test/generators/java/presets/CommonPreset.spec.ts
+++ b/test/generators/java/presets/CommonPreset.spec.ts
@@ -39,6 +39,7 @@ describe('JAVA_COMMON_PRESET', () => {
               equal: true,
               hashCode: true,
               classToString: true,
+              marshalling: false,
             }
           }] 
         }
@@ -50,11 +51,12 @@ describe('JAVA_COMMON_PRESET', () => {
       expect(classModel.result).toMatchSnapshot();
       expect(classModel.dependencies.includes('import java.util.Objects;')).toEqual(true);
     });
-    test('should not render any functions when all 3 options are disabled', async () => {
+    test('should not render any functions when all 4 options are disabled', async () => {
       const options: JavaCommonPresetOptions = {
         equal: false,
         hashCode: false,
         classToString: false,
+        marshalling: false,
       };
 
       const generator = new JavaGenerator(
@@ -80,6 +82,7 @@ describe('JAVA_COMMON_PRESET', () => {
               equal: true,
               hashCode: false,
               classToString: false,
+              marshalling: false,
             }
           }] 
         }
@@ -100,6 +103,7 @@ describe('JAVA_COMMON_PRESET', () => {
               equal: false,
               hashCode: true,
               classToString: false,
+              marshalling: false,
             }
           }] 
         }
@@ -120,6 +124,7 @@ describe('JAVA_COMMON_PRESET', () => {
               equal: false,
               hashCode: false,
               classToString: true,
+              marshalling: false,
             }
           }] 
         }
@@ -130,5 +135,27 @@ describe('JAVA_COMMON_PRESET', () => {
       const classModel = await generator.renderClass(model, inputModel);
       expect(classModel.result).toMatchSnapshot();
     });
+    test('should render un/marshal', async () => {
+      const generator = new JavaGenerator(
+        { 
+          presets: [{
+            preset: JAVA_COMMON_PRESET,
+            options: {
+              equal: false,
+              hashCode: false,
+              classToString: false,
+              marshalling: true,
+            }
+          }] 
+        }
+      );
+      const inputModel = await generator.process(doc);
+      const model = inputModel.models['Clazz'];
+      
+      const classModel = await generator.renderClass(model, inputModel);
+      expect(classModel.dependencies.includes('import java.util.Map;')).toEqual(true);
+      expect(classModel.dependencies.includes('import java.util.stream;')).toEqual(true);
+      expect(classModel.dependencies.includes('import org.json.JSONObject;')).toEqual(true);
+    });
   });
 });
diff --git a/test/generators/java/presets/__snapshots__/CommonPreset.spec.ts.snap b/test/generators/java/presets/__snapshots__/CommonPreset.spec.ts.snap
index 5a057d12..3f62d1bc 100644
--- a/test/generators/java/presets/__snapshots__/CommonPreset.spec.ts.snap
+++ b/test/generators/java/presets/__snapshots__/CommonPreset.spec.ts.snap
@@ -144,6 +144,35 @@ exports[`JAVA_COMMON_PRESET should render common function in class by common pre
 }"
 `;
 
+exports[`JAVA_COMMON_PRESET with option should not render any functions when all 4 options are disabled 1`] = `
+"public class Clazz {
+  private Boolean requiredProp;
+  private String stringProp;
+  private Double numberProp;
+  private Boolean booleanProp;
+  private String[] arrayProp;
+  private Map<String, Object> additionalProperties;
+
+  public Boolean getRequiredProp() { return this.requiredProp; }
+  public void setRequiredProp(Boolean requiredProp) { this.requiredProp = requiredProp; }
+
+  public String getStringProp() { return this.stringProp; }
+  public void setStringProp(String stringProp) { this.stringProp = stringProp; }
+
+  public Double getNumberProp() { return this.numberProp; }
+  public void setNumberProp(Double numberProp) { this.numberProp = numberProp; }
+
+  public Boolean getBooleanProp() { return this.booleanProp; }
+  public void setBooleanProp(Boolean booleanProp) { this.booleanProp = booleanProp; }
+
+  public String[] getArrayProp() { return this.arrayProp; }
+  public void setArrayProp(String[] arrayProp) { this.arrayProp = arrayProp; }
+
+  public Map<String, Object> getAdditionalProperties() { return this.additionalProperties; }
+  public void setAdditionalProperties(Map<String, Object> additionalProperties) { this.additionalProperties = additionalProperties; }
+}"
+`;
+
 exports[`JAVA_COMMON_PRESET with option should render all functions 1`] = `
 "public class Clazz {
   private Boolean requiredProp;
@@ -219,36 +248,6 @@ exports[`JAVA_COMMON_PRESET with option should render all functions 1`] = `
 }"
 `;
 
-
-exports[`JAVA_COMMON_PRESET with option should not render any functions when all 3 options are disabled 1`] = `
-"public class Clazz {
-  private Boolean requiredProp;
-  private String stringProp;
-  private Double numberProp;
-  private Boolean booleanProp;
-  private String[] arrayProp;
-  private Map<String, Object> additionalProperties;
-
-  public Boolean getRequiredProp() { return this.requiredProp; }
-  public void setRequiredProp(Boolean requiredProp) { this.requiredProp = requiredProp; }
-
-  public String getStringProp() { return this.stringProp; }
-  public void setStringProp(String stringProp) { this.stringProp = stringProp; }
-
-  public Double getNumberProp() { return this.numberProp; }
-  public void setNumberProp(Double numberProp) { this.numberProp = numberProp; }
-
-  public Boolean getBooleanProp() { return this.booleanProp; }
-  public void setBooleanProp(Boolean booleanProp) { this.booleanProp = booleanProp; }
-
-  public String[] getArrayProp() { return this.arrayProp; }
-  public void setArrayProp(String[] arrayProp) { this.arrayProp = arrayProp; }
-
-  public Map<String, Object> getAdditionalProperties() { return this.additionalProperties; }
-  public void setAdditionalProperties(Map<String, Object> additionalProperties) { this.additionalProperties = additionalProperties; }
-}"
-`;
-
 exports[`JAVA_COMMON_PRESET with option should render classToString 1`] = `
 "public class Clazz {
   private Boolean requiredProp;