Repository: modiimedia/arri (language: rust)

Issue:
feature: arri generators should be able to generate objects that have no properties
a schema like this

```typescript
const EmptyObject = a.object({});
```

Will cause arri generators to output invalid code. Adding support for empty objects is useful because it makes it easier to have a fallback value in the case of discriminator schemas.

```typescript
const Shape = a.discriminator("Shape", {
  UNKNOWN: a.object({}), // fallback value
  RECT: a.object({
    width: a.float64(),
    height: a.float64(),
  }),
  CIRCLE: a.object({
    radius: a.float64(),
  }),
});

```

Without this we have to add some dummy field to the `UNKNOWN` option which isn't great.


Discussion / hints:

Fix patch:
diff --git a/languages/dart/dart-codegen-reference/lib/reference_client.dart b/languages/dart/dart-codegen-reference/lib/reference_client.dart
index 769c4456..c11f85bd 100644
--- a/languages/dart/dart-codegen-reference/lib/reference_client.dart
+++ b/languages/dart/dart-codegen-reference/lib/reference_client.dart
@@ -138,6 +138,60 @@ class ExampleClientBooksService {
   }
 }
 
+class EmptyObject implements ArriModel {
+  const EmptyObject();
+
+  factory EmptyObject.empty() {
+    return EmptyObject();
+  }
+
+  factory EmptyObject.fromJson(Map<String, dynamic> _input_) {
+    return EmptyObject();
+  }
+
+  factory EmptyObject.fromJsonString(String input) {
+    return EmptyObject.fromJson(json.decode(input));
+  }
+
+  @override
+  Map<String, dynamic> toJson() {
+    final _output_ = <String, dynamic>{};
+    return _output_;
+  }
+
+  @override
+  String toJsonString() {
+    return json.encode(toJson());
+  }
+
+  @override
+  String toUrlQueryParams() {
+    final _queryParts_ = <String>[];
+    return _queryParts_.join("&");
+  }
+
+  @override
+  EmptyObject copyWith() {
+    return EmptyObject();
+  }
+
+  @override
+  List<Object?> get props => [];
+
+  @override
+  bool operator ==(Object other) {
+    return other is EmptyObject && listsAreEqual(props, other.props);
+  }
+
+  @override
+  int get hashCode => listToHashCode(props);
+
+  @override
+  String toString() {
+    return "EmptyObject ${toJsonString()}";
+  }
+}
+
 /// This is a book
 class Book implements ArriModel {
   /// The book ID
diff --git a/languages/dart/dart-codegen/src/object.ts b/languages/dart/dart-codegen/src/object.ts
index 315628bc..1d7b0a69 100644
--- a/languages/dart/dart-codegen/src/object.ts
+++ b/languages/dart/dart-codegen/src/object.ts
@@ -166,11 +166,13 @@ export function dartClassFromSchema(
 `;
     }
 
+    const hasProperties = propNames.length > 0;
+
     result.content = `${getCodeComments(schema.metadata)}class ${finalClassName} implements ${context.discriminatorParentId ? `${context.modelPrefix}${context.discriminatorParentId}` : 'ArriModel'} {
 ${fieldParts.join('\n')}
-  const ${finalClassName}({
+  const ${finalClassName}(${hasProperties ? '{' : ''}
 ${constructorParts.join('\n')}
-  });
+  ${hasProperties ? '}' : ''});
 ${discriminatorPart}
   factory ${finalClassName}.empty() {
     return ${finalClassName}(
@@ -211,9 +213,9 @@ ${toUrlQueryParts.join('\n')}
   }
 
   @override
-  ${finalClassName} copyWith({
+  ${finalClassName} copyWith(${hasProperties ? '{' : ''}
 ${copyWithParamParts.join('\n')}
-  }) {
+  ${hasProperties ? '}' : ''}) {
     return ${finalClassName}(
 ${copyWithReturnParts.join('\n')}
     );
diff --git a/languages/go/go-server/type_def.go b/languages/go/go-server/type_def.go
index 72350abe..45e70783 100644
--- a/languages/go/go-server/type_def.go
+++ b/languages/go/go-server/type_def.go
@@ -249,7 +249,11 @@ func IsDiscriminatorStruct(input reflect.Type) bool {
 	if input.Kind() != reflect.Struct {
 		return false
 	}
-	for i := 0; i < input.NumField(); i++ {
+	numFields := input.NumField()
+	if numFields == 0 {
+		return false
+	}
+	for i := 0; i < numFields; i++ {
 		var discriminatorTag = input.Field(i).Tag.Get("discriminator")
 		if len(discriminatorTag) > 0 {
 			return true
@@ -287,9 +291,9 @@ func structToTypeDef(input reflect.Type, context TypeDefContext) (*TypeDef, erro
 	if kind != reflect.Struct {
 		return nil, errors.ErrUnsupported
 	}
-	if input.NumField() == 0 && input.Name() != "DiscriminatorKey" {
-		return nil, errors.New("cannot create schema for an empty struct")
-	}
+	// if input.NumField() == 0 && input.Name() != "DiscriminatorKey" {
+	// 	return nil, errors.New("cannot create schema for an empty struct")
+	// }
 	requiredFields := OrderedMap[TypeDef]{}
 	optionalFields := OrderedMap[TypeDef]{}
 	for i := 0; i < input.NumField(); i++ {
@@ -424,7 +428,7 @@ func taggedUnionToTypeDef(name Option[string], input reflect.Type, context TypeD
 		return nil, errors.ErrUnsupported
 	}
 	if input.NumField() == 0 {
-		return nil, errors.New("cannot create schema for an empty struct")
+		return nil, errors.New("cannot create discriminator schema for an empty struct")
 	}
 	discriminatorKey := "type"
 	mapping := OrderedMap[TypeDef]{}
diff --git a/languages/kotlin/kotlin-codegen-reference/src/main/kotlin/ExampleClient.kt b/languages/kotlin/kotlin-codegen-reference/src/main/kotlin/ExampleClient.kt
index b533b1a7..7a397d3e 100644
--- a/languages/kotlin/kotlin-codegen-reference/src/main/kotlin/ExampleClient.kt
+++ b/languages/kotlin/kotlin-codegen-reference/src/main/kotlin/ExampleClient.kt
@@ -288,6 +288,48 @@ data class ExampleClientError(
     }
 }
 
+data class EmptyObject(
+    private val placeholderKey: Short = 0
+) : ExampleClientModel {
+    override fun toJson(): String {
+        var output = "{"
+        var hasProperties = false
+        output += "}"
+        return output
+    }
+
+    override fun toUrlQueryParams(): String {
+        val queryParts = mutableListOf<String>()
+        return queryParts.joinToString("&")
+    }
+
+    companion object Factory : ExampleClientModelFactory<EmptyObject> {
+        @JvmStatic
+        override fun new(): EmptyObject {
+            return EmptyObject(
+            )
+        }
+
+        @JvmStatic
+        override fun fromJson(input: String): EmptyObject {
+            return fromJsonElement(JsonInstance.parseToJsonElement(input))
+        }
+
+        @JvmStatic
+        override fun fromJsonElement(__input: JsonElement, instancePath: String): EmptyObject {
+            if (__input !is JsonObject) {
+                __logError("[WARNING] EmptyObject.fromJsonElement() expected kotlinx.serialization.json.JsonObject at $instancePath. Got ${__input.javaClass}. Initializing empty EmptyObject.")
+                return new()
+            }
+
+            return EmptyObject(
+
+            )
+        }
+
+    }
+}
+
 /**
  * This is a book
  */
diff --git a/languages/kotlin/kotlin-codegen/src/object.ts b/languages/kotlin/kotlin-codegen/src/object.ts
index 551c328e..a80ac47d 100644
--- a/languages/kotlin/kotlin-codegen/src/object.ts
+++ b/languages/kotlin/kotlin-codegen/src/object.ts
@@ -169,8 +169,9 @@ ${isLast ? '' : '    hasProperties = true'}\n}`);
     if (context.discriminatorKey && context.discriminatorValue) {
         discriminatorField = `\n    override val ${kotlinIdentifier(context.discriminatorKey)} get() = "${context.discriminatorValue}"\n`;
     }
+    const hasProperties = fieldParts.length > 0;
     const content = `${getCodeComment(schema.metadata, '', 'class')}data class ${prefixedClassName}(
-${fieldParts.join('\n')}
+${fieldParts.join('\n')}${!hasProperties ? '    private val placeholderKey: Short = 0' : ''}
 ) : ${implementedClass} {${discriminatorField}
     override fun toJson(): String {
 ${toJsonParts.join('\n')}    
@@ -201,7 +202,7 @@ ${defaultParts.join('\n')}
             }
 ${fromJsonParts.join('\n')}
             return ${prefixedClassName}(
-                ${kotlinKeys.join(',\n                ')},
+                ${kotlinKeys.join(',\n                ')}${hasProperties ? `,` : ''}
             )
         }
     }
diff --git a/languages/rust/rust-codegen-reference/src/example_client.rs b/languages/rust/rust-codegen-reference/src/example_client.rs
index 463ea730..5702e428 100644
--- a/languages/rust/rust-codegen-reference/src/example_client.rs
+++ b/languages/rust/rust-codegen-reference/src/example_client.rs
@@ -133,6 +133,41 @@ impl ExampleClientBooksService {
     }
 }
 
+#[derive(Clone, Debug, PartialEq)]
+pub struct EmptyObject {}
+
+impl ArriModel for EmptyObject {
+    fn new() -> Self {
+        Self {}
+    }
+
+    fn from_json(input: serde_json::Value) -> Self {
+        match input {
+            serde_json::Value::Object(_val_) => Self {},
+            _ => Self::new(),
+        }
+    }
+
+    fn from_json_string(input: String) -> Self {
+        match serde_json::from_str(input.as_str()) {
+            Ok(val) => Self::from_json(val),
+            _ => Self::new(),
+        }
+    }
+
+    fn to_json_string(&self) -> String {
+        let mut _json_output_ = "{".to_string();
+        let mut _has_keys_ = false;
+        _json_output_.push('}');
+        _json_output_
+    }
+
+    fn to_query_params_string(&self) -> String {
+        let mut _query_parts_: Vec<String> = Vec::new();
+        _query_parts_.join("&")
+    }
+}
+
 /// This is a book
 #[derive(Clone, Debug, PartialEq)]
 pub struct Book {
diff --git a/languages/rust/rust-codegen/src/_common.ts b/languages/rust/rust-codegen/src/_common.ts
index 7475f58b..1cefc9f6 100644
--- a/languages/rust/rust-codegen/src/_common.ts
+++ b/languages/rust/rust-codegen/src/_common.ts
@@ -154,3 +154,10 @@ export function formatDescriptionComment(
         .map((line) => `${leading}/// ${line}`)
         .join('\n');
 }
+
+export function maybeStr(show: boolean, char: string) {
+    if (show) {
+        return char;
+    }
+    return '';
+}
diff --git a/languages/rust/rust-codegen/src/object.ts b/languages/rust/rust-codegen/src/object.ts
index 914017f1..2aea29f8 100644
--- a/languages/rust/rust-codegen/src/object.ts
+++ b/languages/rust/rust-codegen/src/object.ts
@@ -8,6 +8,7 @@ import {
     formatDescriptionComment,
     GeneratorContext,
     getTypeName,
+    maybeStr,
     outputIsOptionType,
     RustProperty,
     validRustIdentifier,
@@ -213,15 +214,16 @@ export default function rustObjectFromSchema(
     if (schema.metadata?.isDeprecated) {
         leading += `#[deprecated]\n`;
     }
+    const hasProperties = fieldDeclarationParts.length > 0;
     result.content = `${leading}#[derive(Clone, Debug, PartialEq)]
 pub struct ${prefixedStructName} {
-${fieldDeclarationParts.join(',\n')},
+${fieldDeclarationParts.join(',\n')}${maybeStr(hasProperties, ',')}
 }
 
 impl ArriModel for ${prefixedStructName} {
     fn new() -> Self {
         Self {
-${defaultParts.join(',\n')},
+${defaultParts.join(',\n')}${maybeStr(hasProperties, ',')}
         }
     }
     fn from_json(input: serde_json::Value) -> Self {
diff --git a/languages/swift/swift-codegen-reference/Sources/SwiftCodegenReference/SwiftCodegenReference.swift b/languages/swift/swift-codegen-reference/Sources/SwiftCodegenReference/SwiftCodegenReference.swift
index 86493963..ee5b81da 100644
--- a/languages/swift/swift-codegen-reference/Sources/SwiftCodegenReference/SwiftCodegenReference.swift
+++ b/languages/swift/swift-codegen-reference/Sources/SwiftCodegenReference/SwiftCodegenReference.swift
@@ -107,6 +107,47 @@ public class ExampleClientBooksService {
     }
 }
 
+public struct EmptyObject: ArriClientModel {
+    public init(
+        
+    ) {
+
+    }
+    public init(json: JSON) {
+    }
+    public init(JSONData: Data) {
+        do {
+            let json = try JSON(data: JSONData)
+            self.init(json: json)
+        } catch {
+            print("[WARNING] Error parsing JSON: \(error)")
+            self.init()
+        }
+    }
+    public init(JSONString: String) {
+        do {
+            let json = try JSON(data: JSONString.data(using: .utf8) ?? Data())
+            self.init(json: json)
+        } catch {
+            print("[WARNING] Error parsing JSON: \(error)")
+            self.init()
+        }
+    }
+    public func toJSONString() -> String {
+        var __json = "{"
+        var __numKeys = 0
+        __json += "}"
+        return __json
+    }
+    public func toURLQueryParts() -> [URLQueryItem] {
+       return []
+    }
+    public func clone() -> EmptyObject {
+        return EmptyObject(
+        )
+    }
+}
+
 /// This is a book
 public struct Book: ArriClientModel {
     /// The book ID
diff --git a/languages/swift/swift-codegen-reference/project.json b/languages/swift/swift-codegen-reference/project.json
index 4c7c4086..1ddc66cf 100644
--- a/languages/swift/swift-codegen-reference/project.json
+++ b/languages/swift/swift-codegen-reference/project.json
@@ -1,8 +1,10 @@
 {
   "name": "swift-codegen-reference",
   "$schema": "../../../node_modules/nx/schemas/project-schema.json",
+  "implicitDependencies": ["swift-client"],
   "targets": {
     "compile": {
+      "dependsOn": ["^compile"],
       "executor": "nx:run-commands",
       "inputs": [
         "{projectRoot}/Sources",
diff --git a/languages/swift/swift-codegen/src/object.ts b/languages/swift/swift-codegen/src/object.ts
index 5e76c9a1..bbe781c1 100644
--- a/languages/swift/swift-codegen/src/object.ts
+++ b/languages/swift/swift-codegen/src/object.ts
@@ -253,6 +253,7 @@ export function swiftObjectFromSchema(
 ${fieldNames.map((field) => `               lhs.${field} == rhs.${field}`).join(' &&\n')}
         }`;
     }
+    const hasProperties = initArgParts.length > 0;
     result.content = `${codeComments(schema)}public ${declaration} ${prefixedTypeName}: ArriClientModel {
 ${fieldNameParts.join('\n')}
     ${initPrefix} init(
@@ -260,7 +261,7 @@ ${initArgParts.join(',\n')}
     ) {
 ${initBodyParts.join('\n')}
     }
-    ${initPrefix} init() {}
+    ${hasProperties ? `${initPrefix} init() {}` : ''}
     ${initPrefix} init(json: JSON) {
 ${initFromJsonParts.join('\n')}
     }
diff --git a/languages/ts/ts-codegen-reference/src/referenceClient.ts b/languages/ts/ts-codegen-reference/src/referenceClient.ts
index 0429f4ea..853bc9fb 100644
--- a/languages/ts/ts-codegen-reference/src/referenceClient.ts
+++ b/languages/ts/ts-codegen-reference/src/referenceClient.ts
@@ -157,6 +157,32 @@ export class ExampleClientBooksService {
     }
 }
 
+export interface EmptyObject {}
+export const $$EmptyObject: ArriModelValidator<EmptyObject> = {
+    new(): EmptyObject {
+        return {};
+    },
+    validate(input): input is EmptyObject {
+        return isObject(input);
+    },
+    fromJson(input): EmptyObject {
+        return {};
+    },
+    fromJsonString(input): EmptyObject {
+        return $$EmptyObject.fromJson(JSON.parse(input));
+    },
+    toJsonString(input): string {
+        let json = '{';
+        let _hasKey = false;
+        json += '}';
+        return json;
+    },
+    toUrlQueryString(input): string {
+        const queryParts: string[] = [];
+        return queryParts.join('&');
+    },
+};
+
 /**
  * This is a book
  */


Test patch:
diff --git a/internal/scripts/generate-test-utils.ts b/internal/scripts/generate-test-utils.ts
index 2de2b1c4..38cb0b9e 100644
--- a/internal/scripts/generate-test-utils.ts
+++ b/internal/scripts/generate-test-utils.ts
@@ -39,6 +39,8 @@ const Discriminator = a.discriminator(
 );
 type Discriminator = a.infer<typeof Discriminator>;
 
+const EmptyObject = a.object({});
+
 const ObjectWithEveryType = a.object(
     {
         string: a.string(),
@@ -181,6 +183,7 @@ const def = createAppDefinition({
         },
     },
     definitions: {
+        EmptyObject,
         Book,
         BookParams,
         NestedObject,
@@ -209,6 +212,11 @@ async function main() {
 
     const targetDate = new Date('01/01/2001 10:00 AM CST');
 
+    files.push({
+        filename: 'EmptyObject.json',
+        content: a.serializeUnsafe(EmptyObject, {}),
+    });
+
     const book: Book = {
         id: '1',
         name: 'The Adventures of Tom Sawyer',
@@ -217,7 +225,7 @@ async function main() {
     };
     files.push({
         filename: 'Book.json',
-        content: a.serialize(Book, book),
+        content: a.serializeUnsafe(Book, book),
     });
 
     const bookParams: BookParams = {
@@ -225,7 +233,7 @@ async function main() {
     };
     files.push({
         filename: 'BookParams.json',
-        content: a.serialize(BookParams, bookParams),
+        content: a.serializeUnsafe(BookParams, bookParams),
     });
 
     const nestedObject: NestedObject = {
@@ -234,7 +242,7 @@ async function main() {
     };
     files.push({
         filename: `NestedObject_NoSpecialChars.json`,
-        content: a.serialize(NestedObject, nestedObject),
+        content: a.serializeUnsafe(NestedObject, nestedObject),
     });
 
     const nestedObjectWithSpecialChars: NestedObject = {
@@ -244,7 +252,7 @@ async function main() {
     };
     files.push({
         filename: 'NestedObject_SpecialChars.json',
-        content: a.serialize(NestedObject, nestedObjectWithSpecialChars),
+        content: a.serializeUnsafe(NestedObject, nestedObjectWithSpecialChars),
     });
 
     const objectWithEveryFieldValue: ObjectWithEveryType = {
@@ -281,17 +289,23 @@ async function main() {
     };
     files.push({
         filename: 'ObjectWithEveryType.json',
-        content: a.serialize(ObjectWithEveryType, objectWithEveryFieldValue),
+        content: a.serializeUnsafe(
+            ObjectWithEveryType,
+            objectWithEveryFieldValue,
+        ),
     });
     objectWithEveryFieldValue.record = { B: false, A: true };
     files.push({
         filename: 'ObjectWithEveryType_ReversedRecord.json',
-        content: a.serialize(ObjectWithEveryType, objectWithEveryFieldValue),
+        content: a.serializeUnsafe(
+            ObjectWithEveryType,
+            objectWithEveryFieldValue,
+        ),
     });
     const objectWithOptionalFieldsAllUndefined: ObjectWithOptionalFields = {};
     files.push({
         filename: 'ObjectWithOptionalFields_AllUndefined.json',
-        content: a.serialize(
+        content: a.serializeUnsafe(
             ObjectWithOptionalFields,
             objectWithOptionalFieldsAllUndefined,
         ),
@@ -302,7 +316,7 @@ async function main() {
     };
     files.push({
         filename: 'ObjectWithOptionalFields_NoUndefined.json',
-        content: a.serialize(
+        content: a.serializeUnsafe(
             ObjectWithOptionalFields,
             objectWithOptionalFieldsNoUndefined,
         ),
@@ -313,7 +327,7 @@ async function main() {
     };
     files.push({
         filename: 'ObjectWithOptionalFields_NoUndefined_ReversedRecord.json',
-        content: a.serialize(
+        content: a.serializeUnsafe(
             ObjectWithOptionalFields,
             objectWithOptionalFieldsNoUndefined,
         ),
@@ -341,7 +355,7 @@ async function main() {
     };
     files.push({
         filename: `ObjectWithNullableFields_AllNull.json`,
-        content: a.serialize(
+        content: a.serializeUnsafe(
             ObjectWithNullableFields,
             objectWithNullableFieldsAllNull,
         ),
@@ -382,7 +396,7 @@ async function main() {
     };
     files.push({
         filename: 'ObjectWithNullableFields_NoNull.json',
-        content: a.serialize(
+        content: a.serializeUnsafe(
             ObjectWithNullableFields,
             objectWithNullableFieldsNoNull,
         ),
@@ -393,7 +407,7 @@ async function main() {
     };
     files.push({
         filename: 'ObjectWithNullableFields_NoNull_ReversedRecord.json',
-        content: a.serialize(
+        content: a.serializeUnsafe(
             ObjectWithNullableFields,
             objectWithNullableFieldsNoNull,
         ),
@@ -417,8 +431,9 @@ async function main() {
     };
     files.push({
         filename: 'RecursiveObject.json',
-        content: a.serialize(RecursiveObject, recursiveObject),
+        content: a.serializeUnsafe(RecursiveObject, recursiveObject),
     });
+
     const mdParts: string[] = [
         'Below are all of the contents of the test JSON files in an easier to read format. Since all of the test files are minified.',
         '',
diff --git a/internal/scripts/run-integration-tests.ts b/internal/scripts/run-integration-tests.ts
index 712e432b..826e649c 100644
--- a/internal/scripts/run-integration-tests.ts
+++ b/internal/scripts/run-integration-tests.ts
@@ -25,7 +25,7 @@ const run = defineCommand({
         },
     },
     async run({ args }) {
-        const parsedArgs = a.parse(CliArgs, args);
+        const parsedArgs = a.parseUnsafe(CliArgs, args);
         if (!parsedArgs.server) {
             const { type } = await enquirer.prompt<{ type: 'ts' | 'go' }>([
                 {
diff --git a/tests/clients/dart/lib/test_client.g.dart b/tests/clients/dart/lib/test_client.g.dart
index 10579798..9a01e91d 100644
--- a/tests/clients/dart/lib/test_client.g.dart
+++ b/tests/clients/dart/lib/test_client.g.dart
@@ -117,6 +117,20 @@ class TestClientTestsService {
     );
   }
 
+  Future<DiscriminatorWithEmptyObject> sendDiscriminatorWithEmptyObject(
+      DiscriminatorWithEmptyObject params) async {
+    return parsedArriRequest(
+      "$_baseUrl/rpcs/tests/send-discriminator-with-empty-object",
+      method: HttpMethod.post,
+      httpClient: _httpClient,
+      headers: _headers,
+      clientVersion: _clientVersion,
+      params: params.toJson(),
+      parser: (body) => DiscriminatorWithEmptyObject.fromJsonString(body),
+      onError: _onError,
+    );
+  }
+
   Future<void> sendError(SendErrorParams params) async {
     return parsedArriRequest(
       "$_baseUrl/rpcs/tests/send-error",
@@ -761,6 +775,195 @@ class DeprecatedRpcParams implements ArriModel {
   }
 }
 
+sealed class DiscriminatorWithEmptyObject implements ArriModel {
+  String get type;
+  const DiscriminatorWithEmptyObject();
+
+  factory DiscriminatorWithEmptyObject.empty() {
+    return DiscriminatorWithEmptyObjectEmpty.empty();
+  }
+
+  factory DiscriminatorWithEmptyObject.fromJson(Map<String, dynamic> _input_) {
+    final type = typeFromDynamic<String>(_input_["type"], "");
+    switch (type) {
+      case "EMPTY":
+        return DiscriminatorWithEmptyObjectEmpty.fromJson(_input_);
+      case "NOT_EMPTY":
+        return DiscriminatorWithEmptyObjectNotEmpty.fromJson(_input_);
+      default:
+        return DiscriminatorWithEmptyObject.empty();
+    }
+  }
+
+  factory DiscriminatorWithEmptyObject.fromJsonString(String input) {
+    return DiscriminatorWithEmptyObject.fromJson(json.decode(input));
+  }
+}
+
+class DiscriminatorWithEmptyObjectEmpty
+    implements DiscriminatorWithEmptyObject {
+  const DiscriminatorWithEmptyObjectEmpty();
+
+  @override
+  String get type => "EMPTY";
+
+  factory DiscriminatorWithEmptyObjectEmpty.empty() {
+    return DiscriminatorWithEmptyObjectEmpty();
+  }
+
+  factory DiscriminatorWithEmptyObjectEmpty.fromJson(
+      Map<String, dynamic> _input_) {
+    return DiscriminatorWithEmptyObjectEmpty();
+  }
+
+  factory DiscriminatorWithEmptyObjectEmpty.fromJsonString(String input) {
+    return DiscriminatorWithEmptyObjectEmpty.fromJson(json.decode(input));
+  }
+
+  @override
+  Map<String, dynamic> toJson() {
+    final _output_ = <String, dynamic>{
+      "type": type,
+    };
+
+    return _output_;
+  }
+
+  @override
+  String toJsonString() {
+    return json.encode(toJson());
+  }
+
+  @override
+  String toUrlQueryParams() {
+    final _queryParts_ = <String>[];
+    _queryParts_.add("type=$type");
+    return _queryParts_.join("&");
+  }
+
+  @override
+  DiscriminatorWithEmptyObjectEmpty copyWith() {
+    return DiscriminatorWithEmptyObjectEmpty();
+  }
+
+  @override
+  List<Object?> get props => [];
+
+  @override
+  bool operator ==(Object other) {
+    return other is DiscriminatorWithEmptyObjectEmpty &&
+        listsAreEqual(props, other.props);
+  }
+
+  @override
+  int get hashCode => listToHashCode(props);
+
+  @override
+  String toString() {
+    return "DiscriminatorWithEmptyObjectEmpty ${toJsonString()}";
+  }
+}
+
+class DiscriminatorWithEmptyObjectNotEmpty
+    implements DiscriminatorWithEmptyObject {
+  final String foo;
+  final double bar;
+  final bool baz;
+  const DiscriminatorWithEmptyObjectNotEmpty({
+    required this.foo,
+    required this.bar,
+    required this.baz,
+  });
+
+  @override
+  String get type => "NOT_EMPTY";
+
+  factory DiscriminatorWithEmptyObjectNotEmpty.empty() {
+    return DiscriminatorWithEmptyObjectNotEmpty(
+      foo: "",
+      bar: 0.0,
+      baz: false,
+    );
+  }
+
+  factory DiscriminatorWithEmptyObjectNotEmpty.fromJson(
+      Map<String, dynamic> _input_) {
+    final foo = typeFromDynamic<String>(_input_["foo"], "");
+    final bar = doubleFromDynamic(_input_["bar"], 0.0);
+    final baz = typeFromDynamic<bool>(_input_["baz"], false);
+    return DiscriminatorWithEmptyObjectNotEmpty(
+      foo: foo,
+      bar: bar,
+      baz: baz,
+    );
+  }
+
+  factory DiscriminatorWithEmptyObjectNotEmpty.fromJsonString(String input) {
+    return DiscriminatorWithEmptyObjectNotEmpty.fromJson(json.decode(input));
+  }
+
+  @override
+  Map<String, dynamic> toJson() {
+    final _output_ = <String, dynamic>{
+      "type": type,
+      "foo": foo,
+      "bar": bar,
+      "baz": baz,
+    };
+
+    return _output_;
+  }
+
+  @override
+  String toJsonString() {
+    return json.encode(toJson());
+  }
+
+  @override
+  String toUrlQueryParams() {
+    final _queryParts_ = <String>[];
+    _queryParts_.add("type=$type");
+    _queryParts_.add("foo=$foo");
+    _queryParts_.add("bar=$bar");
+    _queryParts_.add("baz=$baz");
+    return _queryParts_.join("&");
+  }
+
+  @override
+  DiscriminatorWithEmptyObjectNotEmpty copyWith({
+    String? foo,
+    double? bar,
+    bool? baz,
+  }) {
+    return DiscriminatorWithEmptyObjectNotEmpty(
+      foo: foo ?? this.foo,
+      bar: bar ?? this.bar,
+      baz: baz ?? this.baz,
+    );
+  }
+
+  @override
+  List<Object?> get props => [
+        foo,
+        bar,
+        baz,
+      ];
+
+  @override
+  bool operator ==(Object other) {
+    return other is DiscriminatorWithEmptyObjectNotEmpty &&
+        listsAreEqual(props, other.props);
+  }
+
+  @override
+  int get hashCode => listToHashCode(props);
+
+  @override
+  String toString() {
+    return "DiscriminatorWithEmptyObjectNotEmpty ${toJsonString()}";
+  }
+}
+
 class SendErrorParams implements ArriModel {
   final int code;
   final String message;
diff --git a/tests/clients/dart/lib/test_client_prefixed.g.dart b/tests/clients/dart/lib/test_client_prefixed.g.dart
index f1095158..3746d415 100644
--- a/tests/clients/dart/lib/test_client_prefixed.g.dart
+++ b/tests/clients/dart/lib/test_client_prefixed.g.dart
@@ -117,6 +117,20 @@ class TestClientPrefixedTestsService {
     );
   }
 
+  Future<FooDiscriminatorWithEmptyObject> sendDiscriminatorWithEmptyObject(
+      FooDiscriminatorWithEmptyObject params) async {
+    return parsedArriRequest(
+      "$_baseUrl/rpcs/tests/send-discriminator-with-empty-object",
+      method: HttpMethod.post,
+      httpClient: _httpClient,
+      headers: _headers,
+      clientVersion: _clientVersion,
+      params: params.toJson(),
+      parser: (body) => FooDiscriminatorWithEmptyObject.fromJsonString(body),
+      onError: _onError,
+    );
+  }
+
   Future<void> sendError(FooSendErrorParams params) async {
     return parsedArriRequest(
       "$_baseUrl/rpcs/tests/send-error",
@@ -772,6 +786,196 @@ class FooDeprecatedRpcParams implements ArriModel {
   }
 }
 
+sealed class FooDiscriminatorWithEmptyObject implements ArriModel {
+  String get type;
+  const FooDiscriminatorWithEmptyObject();
+
+  factory FooDiscriminatorWithEmptyObject.empty() {
+    return FooDiscriminatorWithEmptyObjectEmpty.empty();
+  }
+
+  factory FooDiscriminatorWithEmptyObject.fromJson(
+      Map<String, dynamic> _input_) {
+    final type = typeFromDynamic<String>(_input_["type"], "");
+    switch (type) {
+      case "EMPTY":
+        return FooDiscriminatorWithEmptyObjectEmpty.fromJson(_input_);
+      case "NOT_EMPTY":
+        return FooDiscriminatorWithEmptyObjectNotEmpty.fromJson(_input_);
+      default:
+        return FooDiscriminatorWithEmptyObject.empty();
+    }
+  }
+
+  factory FooDiscriminatorWithEmptyObject.fromJsonString(String input) {
+    return FooDiscriminatorWithEmptyObject.fromJson(json.decode(input));
+  }
+}
+
+class FooDiscriminatorWithEmptyObjectEmpty
+    implements FooDiscriminatorWithEmptyObject {
+  const FooDiscriminatorWithEmptyObjectEmpty();
+
+  @override
+  String get type => "EMPTY";
+
+  factory FooDiscriminatorWithEmptyObjectEmpty.empty() {
+    return FooDiscriminatorWithEmptyObjectEmpty();
+  }
+
+  factory FooDiscriminatorWithEmptyObjectEmpty.fromJson(
+      Map<String, dynamic> _input_) {
+    return FooDiscriminatorWithEmptyObjectEmpty();
+  }
+
+  factory FooDiscriminatorWithEmptyObjectEmpty.fromJsonString(String input) {
+    return FooDiscriminatorWithEmptyObjectEmpty.fromJson(json.decode(input));
+  }
+
+  @override
+  Map<String, dynamic> toJson() {
+    final _output_ = <String, dynamic>{
+      "type": type,
+    };
+
+    return _output_;
+  }
+
+  @override
+  String toJsonString() {
+    return json.encode(toJson());
+  }
+
+  @override
+  String toUrlQueryParams() {
+    final _queryParts_ = <String>[];
+    _queryParts_.add("type=$type");
+    return _queryParts_.join("&");
+  }
+
+  @override
+  FooDiscriminatorWithEmptyObjectEmpty copyWith() {
+    return FooDiscriminatorWithEmptyObjectEmpty();
+  }
+
+  @override
+  List<Object?> get props => [];
+
+  @override
+  bool operator ==(Object other) {
+    return other is FooDiscriminatorWithEmptyObjectEmpty &&
+        listsAreEqual(props, other.props);
+  }
+
+  @override
+  int get hashCode => listToHashCode(props);
+
+  @override
+  String toString() {
+    return "FooDiscriminatorWithEmptyObjectEmpty ${toJsonString()}";
+  }
+}
+
+class FooDiscriminatorWithEmptyObjectNotEmpty
+    implements FooDiscriminatorWithEmptyObject {
+  final String foo;
+  final double bar;
+  final bool baz;
+  const FooDiscriminatorWithEmptyObjectNotEmpty({
+    required this.foo,
+    required this.bar,
+    required this.baz,
+  });
+
+  @override
+  String get type => "NOT_EMPTY";
+
+  factory FooDiscriminatorWithEmptyObjectNotEmpty.empty() {
+    return FooDiscriminatorWithEmptyObjectNotEmpty(
+      foo: "",
+      bar: 0.0,
+      baz: false,
+    );
+  }
+
+  factory FooDiscriminatorWithEmptyObjectNotEmpty.fromJson(
+      Map<String, dynamic> _input_) {
+    final foo = typeFromDynamic<String>(_input_["foo"], "");
+    final bar = doubleFromDynamic(_input_["bar"], 0.0);
+    final baz = typeFromDynamic<bool>(_input_["baz"], false);
+    return FooDiscriminatorWithEmptyObjectNotEmpty(
+      foo: foo,
+      bar: bar,
+      baz: baz,
+    );
+  }
+
+  factory FooDiscriminatorWithEmptyObjectNotEmpty.fromJsonString(String input) {
+    return FooDiscriminatorWithEmptyObjectNotEmpty.fromJson(json.decode(input));
+  }
+
+  @override
+  Map<String, dynamic> toJson() {
+    final _output_ = <String, dynamic>{
+      "type": type,
+      "foo": foo,
+      "bar": bar,
+      "baz": baz,
+    };
+
+    return _output_;
+  }
+
+  @override
+  String toJsonString() {
+    return json.encode(toJson());
+  }
+
+  @override
+  String toUrlQueryParams() {
+    final _queryParts_ = <String>[];
+    _queryParts_.add("type=$type");
+    _queryParts_.add("foo=$foo");
+    _queryParts_.add("bar=$bar");
+    _queryParts_.add("baz=$baz");
+    return _queryParts_.join("&");
+  }
+
+  @override
+  FooDiscriminatorWithEmptyObjectNotEmpty copyWith({
+    String? foo,
+    double? bar,
+    bool? baz,
+  }) {
+    return FooDiscriminatorWithEmptyObjectNotEmpty(
+      foo: foo ?? this.foo,
+      bar: bar ?? this.bar,
+      baz: baz ?? this.baz,
+    );
+  }
+
+  @override
+  List<Object?> get props => [
+        foo,
+        bar,
+        baz,
+      ];
+
+  @override
+  bool operator ==(Object other) {
+    return other is FooDiscriminatorWithEmptyObjectNotEmpty &&
+        listsAreEqual(props, other.props);
+  }
+
+  @override
+  int get hashCode => listToHashCode(props);
+
+  @override
+  String toString() {
+    return "FooDiscriminatorWithEmptyObjectNotEmpty ${toJsonString()}";
+  }
+}
+
 class FooSendErrorParams implements ArriModel {
   final int code;
   final String message;
diff --git a/tests/clients/kotlin/src/main/kotlin/TestClient.g.kt b/tests/clients/kotlin/src/main/kotlin/TestClient.g.kt
index bb3f6888..359ddc4e 100644
--- a/tests/clients/kotlin/src/main/kotlin/TestClient.g.kt
+++ b/tests/clients/kotlin/src/main/kotlin/TestClient.g.kt
@@ -177,6 +177,33 @@ suspend fun deprecatedRpc(params: DeprecatedRpcParams): Unit {
         }
     }
 
+    suspend fun sendDiscriminatorWithEmptyObject(params: DiscriminatorWithEmptyObject): DiscriminatorWithEmptyObject {
+        try {
+            val response = __prepareRequest(
+                client = httpClient,
+                url = "$baseUrl/rpcs/tests/send-discriminator-with-empty-object",
+                method = HttpMethod.Post,
+                params = params,
+                headers = headers?.invoke(),
+            ).execute()
+            if (response.headers["Content-Type"] != "application/json") {
+            throw TestClientError(
+                code = 0,
+                errorMessage = "Expected server to return Content-Type \"application/json\". Got \"${response.headers["Content-Type"]}\"",
+                data = JsonPrimitive(response.bodyAsText()),
+                stack = null,
+            )
+        }
+            if (response.status.value in 200..299) {
+                return DiscriminatorWithEmptyObject.fromJson(response.bodyAsText())
+            }
+            throw TestClientError.fromJson(response.bodyAsText())    
+        } catch (e: Exception) {
+            onError(e)
+            throw e
+        }
+    }
+
     suspend fun sendError(params: SendErrorParams): Unit {
         try {
             val response = __prepareRequest(
@@ -898,6 +925,159 @@ val deprecatedField: String = when (__input.jsonObject["deprecatedField"]) {
 
 
 
+sealed interface DiscriminatorWithEmptyObject : TestClientModel {
+    val type: String
+
+    companion object Factory : TestClientModelFactory<DiscriminatorWithEmptyObject> {
+        @JvmStatic
+        override fun new(): DiscriminatorWithEmptyObject {
+            return DiscriminatorWithEmptyObjectEmpty.new()
+        }
+
+        @JvmStatic
+        override fun fromJson(input: String): DiscriminatorWithEmptyObject {
+            return fromJsonElement(JsonInstance.parseToJsonElement(input))
+        }
+
+        @JvmStatic
+        override fun fromJsonElement(__input: JsonElement, instancePath: String): DiscriminatorWithEmptyObject {
+            if (__input !is JsonObject) {
+                __logError("[WARNING] Discriminator.fromJsonElement() expected kotlinx.serialization.json.JsonObject at $instancePath. Got ${__input.javaClass}. Initializing empty DiscriminatorWithEmptyObject.")
+                return new()
+            }
+            return when (__input.jsonObject["type"]) {
+                is JsonPrimitive -> when (__input.jsonObject["type"]!!.jsonPrimitive.contentOrNull) {
+                    "EMPTY" -> DiscriminatorWithEmptyObjectEmpty.fromJsonElement(__input, instancePath)
+"NOT_EMPTY" -> DiscriminatorWithEmptyObjectNotEmpty.fromJsonElement(__input, instancePath)
+                    else -> new()
+                }
+
+                else -> new()
+            }
+        }
+    }
+}
+
+data class DiscriminatorWithEmptyObjectEmpty(
+    private val placeholderKey: Short = 0
+) : DiscriminatorWithEmptyObject {
+    override val type get() = "EMPTY"
+
+    override fun toJson(): String {
+var output = "{"
+output += "\"type\":\"EMPTY\""
+output += "}"
+return output    
+    }
+
+    override fun toUrlQueryParams(): String {
+val queryParts = mutableListOf<String>()
+queryParts.add("type=EMPTY")
+return queryParts.joinToString("&")
+    }
+
+    companion object Factory : TestClientModelFactory<DiscriminatorWithEmptyObjectEmpty> {
+        @JvmStatic
+        override fun new(): DiscriminatorWithEmptyObjectEmpty {
+            return DiscriminatorWithEmptyObjectEmpty(
+
+            )
+        }
+
+        @JvmStatic
+        override fun fromJson(input: String): DiscriminatorWithEmptyObjectEmpty {
+            return fromJsonElement(JsonInstance.parseToJsonElement(input))
+        }
+
+        @JvmStatic
+        override fun fromJsonElement(__input: JsonElement, instancePath: String): DiscriminatorWithEmptyObjectEmpty {
+            if (__input !is JsonObject) {
+                __logError("[WARNING] DiscriminatorWithEmptyObjectEmpty.fromJsonElement() expected kotlinx.serialization.json.JsonObject at $instancePath. Got ${__input.javaClass}. Initializing empty DiscriminatorWithEmptyObjectEmpty.")
+                return new()
+            }
+
+            return DiscriminatorWithEmptyObjectEmpty(
+                
+            )
+        }
+    }
+}
+
+
+
+data class DiscriminatorWithEmptyObjectNotEmpty(
+    val foo: String,
+    val bar: Double,
+    val baz: Boolean,
+) : DiscriminatorWithEmptyObject {
+    override val type get() = "NOT_EMPTY"
+
+    override fun toJson(): String {
+var output = "{"
+output += "\"type\":\"NOT_EMPTY\""
+output += ",\"foo\":"
+output += buildString { printQuoted(foo) }
+output += ",\"bar\":"
+output += bar
+output += ",\"baz\":"
+output += baz
+output += "}"
+return output    
+    }
+
+    override fun toUrlQueryParams(): String {
+val queryParts = mutableListOf<String>()
+queryParts.add("type=NOT_EMPTY")
+queryParts.add("foo=$foo")
+queryParts.add("bar=$bar")
+queryParts.add("baz=$baz")
+return queryParts.joinToString("&")
+    }
+
+    companion object Factory : TestClientModelFactory<DiscriminatorWithEmptyObjectNotEmpty> {
+        @JvmStatic
+        override fun new(): DiscriminatorWithEmptyObjectNotEmpty {
+            return DiscriminatorWithEmptyObjectNotEmpty(
+                foo = "",
+                bar = 0.0,
+                baz = false,
+            )
+        }
+
+        @JvmStatic
+        override fun fromJson(input: String): DiscriminatorWithEmptyObjectNotEmpty {
+            return fromJsonElement(JsonInstance.parseToJsonElement(input))
+        }
+
+        @JvmStatic
+        override fun fromJsonElement(__input: JsonElement, instancePath: String): DiscriminatorWithEmptyObjectNotEmpty {
+            if (__input !is JsonObject) {
+                __logError("[WARNING] DiscriminatorWithEmptyObjectNotEmpty.fromJsonElement() expected kotlinx.serialization.json.JsonObject at $instancePath. Got ${__input.javaClass}. Initializing empty DiscriminatorWithEmptyObjectNotEmpty.")
+                return new()
+            }
+val foo: String = when (__input.jsonObject["foo"]) {
+                is JsonPrimitive -> __input.jsonObject["foo"]!!.jsonPrimitive.contentOrNull ?: ""
+                else -> ""
+            }
+val bar: Double = when (__input.jsonObject["bar"]) {
+                is JsonPrimitive -> __input.jsonObject["bar"]!!.jsonPrimitive.doubleOrNull ?: 0.0
+                else -> 0.0
+            }
+val baz: Boolean = when (__input.jsonObject["baz"]) {
+                is JsonPrimitive -> __input.jsonObject["baz"]!!.jsonPrimitive.booleanOrNull ?: false
+                else -> false
+            }
+            return DiscriminatorWithEmptyObjectNotEmpty(
+                foo,
+                bar,
+                baz,
+            )
+        }
+    }
+}
+
+
+
 data class SendErrorParams(
     val code: UShort,
     val message: String,
diff --git a/tests/clients/kotlin/src/main/kotlin/TestClientPrefixed.g.kt b/tests/clients/kotlin/src/main/kotlin/TestClientPrefixed.g.kt
index a665483a..56aa0614 100644
--- a/tests/clients/kotlin/src/main/kotlin/TestClientPrefixed.g.kt
+++ b/tests/clients/kotlin/src/main/kotlin/TestClientPrefixed.g.kt
@@ -177,6 +177,33 @@ suspend fun deprecatedRpc(params: FooDeprecatedRpcParams): Unit {
         }
     }
 
+    suspend fun sendDiscriminatorWithEmptyObject(params: FooDiscriminatorWithEmptyObject): FooDiscriminatorWithEmptyObject {
+        try {
+            val response = __prepareRequest(
+                client = httpClient,
+                url = "$baseUrl/rpcs/tests/send-discriminator-with-empty-object",
+                method = HttpMethod.Post,
+                params = params,
+                headers = headers?.invoke(),
+            ).execute()
+            if (response.headers["Content-Type"] != "application/json") {
+            throw TestClientPrefixedError(
+                code = 0,
+                errorMessage = "Expected server to return Content-Type \"application/json\". Got \"${response.headers["Content-Type"]}\"",
+                data = JsonPrimitive(response.bodyAsText()),
+                stack = null,
+            )
+        }
+            if (response.status.value in 200..299) {
+                return FooDiscriminatorWithEmptyObject.fromJson(response.bodyAsText())
+            }
+            throw TestClientPrefixedError.fromJson(response.bodyAsText())    
+        } catch (e: Exception) {
+            onError(e)
+            throw e
+        }
+    }
+
     suspend fun sendError(params: FooSendErrorParams): Unit {
         try {
             val response = __prepareRequest(
@@ -898,6 +925,159 @@ val deprecatedField: String = when (__input.jsonObject["deprecatedField"]) {
 
 
 
+sealed interface FooDiscriminatorWithEmptyObject : TestClientPrefixedModel {
+    val type: String
+
+    companion object Factory : TestClientPrefixedModelFactory<FooDiscriminatorWithEmptyObject> {
+        @JvmStatic
+        override fun new(): FooDiscriminatorWithEmptyObject {
+            return FooDiscriminatorWithEmptyObjectEmpty.new()
+        }
+
+        @JvmStatic
+        override fun fromJson(input: String): FooDiscriminatorWithEmptyObject {
+            return fromJsonElement(JsonInstance.parseToJsonElement(input))
+        }
+
+        @JvmStatic
+        override fun fromJsonElement(__input: JsonElement, instancePath: String): FooDiscriminatorWithEmptyObject {
+            if (__input !is JsonObject) {
+                __logError("[WARNING] Discriminator.fromJsonElement() expected kotlinx.serialization.json.JsonObject at $instancePath. Got ${__input.javaClass}. Initializing empty FooDiscriminatorWithEmptyObject.")
+                return new()
+            }
+            return when (__input.jsonObject["type"]) {
+                is JsonPrimitive -> when (__input.jsonObject["type"]!!.jsonPrimitive.contentOrNull) {
+                    "EMPTY" -> FooDiscriminatorWithEmptyObjectEmpty.fromJsonElement(__input, instancePath)
+"NOT_EMPTY" -> FooDiscriminatorWithEmptyObjectNotEmpty.fromJsonElement(__input, instancePath)
+                    else -> new()
+                }
+
+                else -> new()
+            }
+        }
+    }
+}
+
+data class FooDiscriminatorWithEmptyObjectEmpty(
+    private val placeholderKey: Short = 0
+) : FooDiscriminatorWithEmptyObject {
+    override val type get() = "EMPTY"
+
+    override fun toJson(): String {
+var output = "{"
+output += "\"type\":\"EMPTY\""
+output += "}"
+return output    
+    }
+
+    override fun toUrlQueryParams(): String {
+val queryParts = mutableListOf<String>()
+queryParts.add("type=EMPTY")
+return queryParts.joinToString("&")
+    }
+
+    companion object Factory : TestClientPrefixedModelFactory<FooDiscriminatorWithEmptyObjectEmpty> {
+        @JvmStatic
+        override fun new(): FooDiscriminatorWithEmptyObjectEmpty {
+            return FooDiscriminatorWithEmptyObjectEmpty(
+
+            )
+        }
+
+        @JvmStatic
+        override fun fromJson(input: String): FooDiscriminatorWithEmptyObjectEmpty {
+            return fromJsonElement(JsonInstance.parseToJsonElement(input))
+        }
+
+        @JvmStatic
+        override fun fromJsonElement(__input: JsonElement, instancePath: String): FooDiscriminatorWithEmptyObjectEmpty {
+            if (__input !is JsonObject) {
+                __logError("[WARNING] FooDiscriminatorWithEmptyObjectEmpty.fromJsonElement() expected kotlinx.serialization.json.JsonObject at $instancePath. Got ${__input.javaClass}. Initializing empty FooDiscriminatorWithEmptyObjectEmpty.")
+                return new()
+            }
+
+            return FooDiscriminatorWithEmptyObjectEmpty(
+                
+            )
+        }
+    }
+}
+
+
+
+data class FooDiscriminatorWithEmptyObjectNotEmpty(
+    val foo: String,
+    val bar: Double,
+    val baz: Boolean,
+) : FooDiscriminatorWithEmptyObject {
+    override val type get() = "NOT_EMPTY"
+
+    override fun toJson(): String {
+var output = "{"
+output += "\"type\":\"NOT_EMPTY\""
+output += ",\"foo\":"
+output += buildString { printQuoted(foo) }
+output += ",\"bar\":"
+output += bar
+output += ",\"baz\":"
+output += baz
+output += "}"
+return output    
+    }
+
+    override fun toUrlQueryParams(): String {
+val queryParts = mutableListOf<String>()
+queryParts.add("type=NOT_EMPTY")
+queryParts.add("foo=$foo")
+queryParts.add("bar=$bar")
+queryParts.add("baz=$baz")
+return queryParts.joinToString("&")
+    }
+
+    companion object Factory : TestClientPrefixedModelFactory<FooDiscriminatorWithEmptyObjectNotEmpty> {
+        @JvmStatic
+        override fun new(): FooDiscriminatorWithEmptyObjectNotEmpty {
+            return FooDiscriminatorWithEmptyObjectNotEmpty(
+                foo = "",
+                bar = 0.0,
+                baz = false,
+            )
+        }
+
+        @JvmStatic
+        override fun fromJson(input: String): FooDiscriminatorWithEmptyObjectNotEmpty {
+            return fromJsonElement(JsonInstance.parseToJsonElement(input))
+        }
+
+        @JvmStatic
+        override fun fromJsonElement(__input: JsonElement, instancePath: String): FooDiscriminatorWithEmptyObjectNotEmpty {
+            if (__input !is JsonObject) {
+                __logError("[WARNING] FooDiscriminatorWithEmptyObjectNotEmpty.fromJsonElement() expected kotlinx.serialization.json.JsonObject at $instancePath. Got ${__input.javaClass}. Initializing empty FooDiscriminatorWithEmptyObjectNotEmpty.")
+                return new()
+            }
+val foo: String = when (__input.jsonObject["foo"]) {
+                is JsonPrimitive -> __input.jsonObject["foo"]!!.jsonPrimitive.contentOrNull ?: ""
+                else -> ""
+            }
+val bar: Double = when (__input.jsonObject["bar"]) {
+                is JsonPrimitive -> __input.jsonObject["bar"]!!.jsonPrimitive.doubleOrNull ?: 0.0
+                else -> 0.0
+            }
+val baz: Boolean = when (__input.jsonObject["baz"]) {
+                is JsonPrimitive -> __input.jsonObject["baz"]!!.jsonPrimitive.booleanOrNull ?: false
+                else -> false
+            }
+            return FooDiscriminatorWithEmptyObjectNotEmpty(
+                foo,
+                bar,
+                baz,
+            )
+        }
+    }
+}
+
+
+
 data class FooSendErrorParams(
     val code: UShort,
     val message: String,
diff --git a/tests/clients/rust/src/test_client.g.rs b/tests/clients/rust/src/test_client.g.rs
index e615d135..fa4b0921 100644
--- a/tests/clients/rust/src/test_client.g.rs
+++ b/tests/clients/rust/src/test_client.g.rs
@@ -150,6 +150,26 @@ impl TestClientTestsService {
         )
         .await
     }
+    pub async fn send_discriminator_with_empty_object(
+        &self,
+        params: DiscriminatorWithEmptyObject,
+    ) -> Result<DiscriminatorWithEmptyObject, ArriError> {
+        parsed_arri_request(
+            ArriParsedRequestOptions {
+                http_client: &self._config.http_client,
+                url: format!(
+                    "{}/rpcs/tests/send-discriminator-with-empty-object",
+                    &self._config.base_url
+                ),
+                method: reqwest::Method::POST,
+                headers: self._config.headers.clone(),
+                client_version: "10".to_string(),
+            },
+            Some(params),
+            |body| return DiscriminatorWithEmptyObject::from_json_string(body),
+        )
+        .await
+    }
     pub async fn send_error(&self, params: SendErrorParams) -> Result<(), ArriError> {
         parsed_arri_request(
             ArriParsedRequestOptions {
@@ -650,6 +670,94 @@ impl ArriModel for DeprecatedRpcParams {
     }
 }
 
+#[derive(Clone, Debug, PartialEq)]
+pub enum DiscriminatorWithEmptyObject {
+    Empty {},
+    NotEmpty { foo: String, bar: f64, baz: bool },
+}
+
+impl ArriModel for DiscriminatorWithEmptyObject {
+    fn new() -> Self {
+        Self::Empty {}
+    }
+
+    fn from_json(input: serde_json::Value) -> Self {
+        match input {
+            serde_json::Value::Object(_val_) => {
+                let r#type = match _val_.get("type") {
+                    Some(serde_json::Value::String(r#type_val)) => r#type_val.to_owned(),
+                    _ => "".to_string(),
+                };
+                match r#type.as_str() {
+                    "EMPTY" => Self::Empty {},
+                    "NOT_EMPTY" => {
+                        let foo = match _val_.get("foo") {
+                            Some(serde_json::Value::String(foo_val)) => foo_val.to_owned(),
+                            _ => "".to_string(),
+                        };
+                        let bar = match _val_.get("bar") {
+                            Some(serde_json::Value::Number(bar_val)) => {
+                                bar_val.as_f64().unwrap_or(0.0)
+                            }
+                            _ => 0.0,
+                        };
+                        let baz = match _val_.get("baz") {
+                            Some(serde_json::Value::Bool(baz_val)) => baz_val.to_owned(),
+                            _ => false,
+                        };
+                        Self::NotEmpty { foo, bar, baz }
+                    }
+                    _ => Self::new(),
+                }
+            }
+            _ => Self::new(),
+        }
+    }
+
+    fn from_json_string(input: String) -> Self {
+        match serde_json::from_str(input.as_str()) {
+            Ok(val) => Self::from_json(val),
+            _ => Self::new(),
+        }
+    }
+
+    fn to_json_string(&self) -> String {
+        let mut _json_output_ = "{".to_string();
+        match &self {
+            Self::Empty {} => {
+                _json_output_.push_str("\"type\":\"EMPTY\"");
+            }
+            Self::NotEmpty { foo, bar, baz } => {
+                _json_output_.push_str("\"type\":\"NOT_EMPTY\"");
+                _json_output_.push_str(",\"foo\":");
+                _json_output_.push_str(serialize_string(foo).as_str());
+                _json_output_.push_str(",\"bar\":");
+                _json_output_.push_str(bar.to_string().as_str());
+                _json_output_.push_str(",\"baz\":");
+                _json_output_.push_str(baz.to_string().as_str());
+            }
+        }
+        _json_output_.push('}');
+        _json_output_
+    }
+
+    fn to_query_params_string(&self) -> String {
+        let mut _query_parts_: Vec<String> = Vec::new();
+        match &self {
+            Self::Empty {} => {
+                _query_parts_.push(format!("type=EMPTY"));
+            }
+            Self::NotEmpty { foo, bar, baz } => {
+                _query_parts_.push(format!("type=NOT_EMPTY"));
+                _query_parts_.push(format!("foo={}", foo));
+                _query_parts_.push(format!("bar={}", bar));
+                _query_parts_.push(format!("baz={}", baz));
+            }
+        }
+        _query_parts_.join("&")
+    }
+}
+
 #[derive(Clone, Debug, PartialEq)]
 pub struct SendErrorParams {
     pub code: u16,
diff --git a/tests/clients/rust/src/test_client_prefixed.g.rs b/tests/clients/rust/src/test_client_prefixed.g.rs
index f63bbf2a..9fa2b7f2 100644
--- a/tests/clients/rust/src/test_client_prefixed.g.rs
+++ b/tests/clients/rust/src/test_client_prefixed.g.rs
@@ -150,6 +150,26 @@ impl TestClientPrefixedTestsService {
         )
         .await
     }
+    pub async fn send_discriminator_with_empty_object(
+        &self,
+        params: FooDiscriminatorWithEmptyObject,
+    ) -> Result<FooDiscriminatorWithEmptyObject, ArriError> {
+        parsed_arri_request(
+            ArriParsedRequestOptions {
+                http_client: &self._config.http_client,
+                url: format!(
+                    "{}/rpcs/tests/send-discriminator-with-empty-object",
+                    &self._config.base_url
+                ),
+                method: reqwest::Method::POST,
+                headers: self._config.headers.clone(),
+                client_version: "10".to_string(),
+            },
+            Some(params),
+            |body| return FooDiscriminatorWithEmptyObject::from_json_string(body),
+        )
+        .await
+    }
     pub async fn send_error(&self, params: FooSendErrorParams) -> Result<(), ArriError> {
         parsed_arri_request(
             ArriParsedRequestOptions {
@@ -650,6 +670,94 @@ impl ArriModel for FooDeprecatedRpcParams {
     }
 }
 
+#[derive(Clone, Debug, PartialEq)]
+pub enum FooDiscriminatorWithEmptyObject {
+    Empty {},
+    NotEmpty { foo: String, bar: f64, baz: bool },
+}
+
+impl ArriModel for FooDiscriminatorWithEmptyObject {
+    fn new() -> Self {
+        Self::Empty {}
+    }
+
+    fn from_json(input: serde_json::Value) -> Self {
+        match input {
+            serde_json::Value::Object(_val_) => {
+                let r#type = match _val_.get("type") {
+                    Some(serde_json::Value::String(r#type_val)) => r#type_val.to_owned(),
+                    _ => "".to_string(),
+                };
+                match r#type.as_str() {
+                    "EMPTY" => Self::Empty {},
+                    "NOT_EMPTY" => {
+                        let foo = match _val_.get("foo") {
+                            Some(serde_json::Value::String(foo_val)) => foo_val.to_owned(),
+                            _ => "".to_string(),
+                        };
+                        let bar = match _val_.get("bar") {
+                            Some(serde_json::Value::Number(bar_val)) => {
+                                bar_val.as_f64().unwrap_or(0.0)
+                            }
+                            _ => 0.0,
+                        };
+                        let baz = match _val_.get("baz") {
+                            Some(serde_json::Value::Bool(baz_val)) => baz_val.to_owned(),
+                            _ => false,
+                        };
+                        Self::NotEmpty { foo, bar, baz }
+                    }
+                    _ => Self::new(),
+                }
+            }
+            _ => Self::new(),
+        }
+    }
+
+    fn from_json_string(input: String) -> Self {
+        match serde_json::from_str(input.as_str()) {
+            Ok(val) => Self::from_json(val),
+            _ => Self::new(),
+        }
+    }
+
+    fn to_json_string(&self) -> String {
+        let mut _json_output_ = "{".to_string();
+        match &self {
+            Self::Empty {} => {
+                _json_output_.push_str("\"type\":\"EMPTY\"");
+            }
+            Self::NotEmpty { foo, bar, baz } => {
+                _json_output_.push_str("\"type\":\"NOT_EMPTY\"");
+                _json_output_.push_str(",\"foo\":");
+                _json_output_.push_str(serialize_string(foo).as_str());
+                _json_output_.push_str(",\"bar\":");
+                _json_output_.push_str(bar.to_string().as_str());
+                _json_output_.push_str(",\"baz\":");
+                _json_output_.push_str(baz.to_string().as_str());
+            }
+        }
+        _json_output_.push('}');
+        _json_output_
+    }
+
+    fn to_query_params_string(&self) -> String {
+        let mut _query_parts_: Vec<String> = Vec::new();
+        match &self {
+            Self::Empty {} => {
+                _query_parts_.push(format!("type=EMPTY"));
+            }
+            Self::NotEmpty { foo, bar, baz } => {
+                _query_parts_.push(format!("type=NOT_EMPTY"));
+                _query_parts_.push(format!("foo={}", foo));
+                _query_parts_.push(format!("bar={}", bar));
+                _query_parts_.push(format!("baz={}", baz));
+            }
+        }
+        _query_parts_.join("&")
+    }
+}
+
 #[derive(Clone, Debug, PartialEq)]
 pub struct FooSendErrorParams {
     pub code: u16,
diff --git a/tests/clients/swift/Sources/TestClient.g.swift b/tests/clients/swift/Sources/TestClient.g.swift
index 1f202e0f..ccd18d78 100644
--- a/tests/clients/swift/Sources/TestClient.g.swift
+++ b/tests/clients/swift/Sources/TestClient.g.swift
@@ -117,6 +117,18 @@ public class TestClientTestsService {
         )
         
     }
+    public func sendDiscriminatorWithEmptyObject(_ params: DiscriminatorWithEmptyObject) async throws -> DiscriminatorWithEmptyObject {
+        let result: DiscriminatorWithEmptyObject = try await parsedArriHttpRequest(
+            delegate: self.delegate,
+            url: "\(self.baseURL)/rpcs/tests/send-discriminator-with-empty-object",
+            method: "POST",
+            headers: self.headers,
+            clientVersion: "10",
+            params: params,
+            onError: onError
+        )
+        return result
+    }
     public func sendError(_ params: SendErrorParams) async throws -> () {
         let _: EmptyArriModel = try await parsedArriHttpRequest(
             delegate: self.delegate,
@@ -506,6 +518,192 @@ public struct DeprecatedRpcParams: ArriClientModel {
 }
     
 
+public enum DiscriminatorWithEmptyObject: ArriClientModel {
+    case empty(DiscriminatorWithEmptyObjectEmpty)
+    case notEmpty(DiscriminatorWithEmptyObjectNotEmpty)
+    public init() {
+        self = .empty(DiscriminatorWithEmptyObjectEmpty())
+    }
+    public init(json: JSON) {
+        let discriminator = json["type"].string ?? ""
+        switch (discriminator) {
+            case "EMPTY":
+                self = .empty(DiscriminatorWithEmptyObjectEmpty(json: json))
+                break
+            case "NOT_EMPTY":
+                self = .notEmpty(DiscriminatorWithEmptyObjectNotEmpty(json: json))
+                break
+            default:
+                self = .empty(DiscriminatorWithEmptyObjectEmpty())
+                break
+        }
+    }
+    public init(JSONData: Data) {
+        do {
+            let json = try JSON(data: JSONData)
+            self.init(json: json)
+        } catch {
+            print("[WARNING] Error parsing JSON: \(error)")
+            self.init()
+        }
+    }
+    public init(JSONString: String) {
+        do {
+            let json = try JSON(data: JSONString.data(using: .utf8) ?? Data())
+            self.init(json: json)
+        } catch {
+            print("[WARNING] Error parsing JSON: \(error)")
+            self.init()
+        }
+    }
+    public func toJSONString() -> String {
+        switch(self) {
+            case .empty(let __innerVal):
+                return __innerVal.toJSONString()
+            case .notEmpty(let __innerVal):
+                return __innerVal.toJSONString()
+        }
+    }
+    public func toURLQueryParts() -> [URLQueryItem] {
+        switch(self) {
+            case .empty(let __innerVal):
+                return __innerVal.toURLQueryParts()
+            case .notEmpty(let __innerVal):
+                return __innerVal.toURLQueryParts()
+        }
+    }
+    public func clone() -> DiscriminatorWithEmptyObject {
+        switch(self) {
+            case .empty(let __innerVal):
+                return .empty(__innerVal.clone())
+            case .notEmpty(let __innerVal):
+                return .notEmpty(__innerVal.clone())
+        }
+    }
+}
+    
+public struct DiscriminatorWithEmptyObjectEmpty: ArriClientModel {
+    let type: String = "EMPTY"
+    public init(
+
+    ) {
+
+    }
+    
+    public init(json: JSON) {
+
+    }
+    public init(JSONData: Data) {
+        do {
+            let json = try JSON(data: JSONData)
+            self.init(json: json)
+        } catch {
+            print("[WARNING] Error parsing JSON: \(error)")
+            self.init()
+        }
+    }
+    public init(JSONString: String) {
+        do {
+            let json = try JSON(data: JSONString.data(using: .utf8) ?? Data())
+            self.init(json: json) 
+        } catch {
+            print("[WARNING] Error parsing JSON: \(error)")
+            self.init()
+        }
+    }
+    public func toJSONString() -> String {
+        var __json = "{"
+
+        __json += "\"type\":\"EMPTY\""
+        __json += "}"
+        return __json
+    }
+    public func toURLQueryParts() -> [URLQueryItem] {
+        var __queryParts: [URLQueryItem] = []
+        __queryParts.append(URLQueryItem(name: "type", value: "EMPTY"))
+        return __queryParts
+    }
+    public func clone() -> DiscriminatorWithEmptyObjectEmpty {
+
+        return DiscriminatorWithEmptyObjectEmpty(
+
+        )
+    }
+    
+}
+    
+
+public struct DiscriminatorWithEmptyObjectNotEmpty: ArriClientModel {
+    let type: String = "NOT_EMPTY"
+    public var foo: String = ""
+    public var bar: Float64 = 0.0
+    public var baz: Bool = false
+    public init(
+        foo: String,
+        bar: Float64,
+        baz: Bool
+    ) {
+            self.foo = foo
+            self.bar = bar
+            self.baz = baz
+    }
+    public init() {}
+    public init(json: JSON) {
+        self.foo = json["foo"].string ?? ""
+        self.bar = json["bar"].double ?? 0.0
+        self.baz = json["baz"].bool ?? false
+    }
+    public init(JSONData: Data) {
+        do {
+            let json = try JSON(data: JSONData)
+            self.init(json: json)
+        } catch {
+            print("[WARNING] Error parsing JSON: \(error)")
+            self.init()
+        }
+    }
+    public init(JSONString: String) {
+        do {
+            let json = try JSON(data: JSONString.data(using: .utf8) ?? Data())
+            self.init(json: json) 
+        } catch {
+            print("[WARNING] Error parsing JSON: \(error)")
+            self.init()
+        }
+    }
+    public func toJSONString() -> String {
+        var __json = "{"
+
+        __json += "\"type\":\"NOT_EMPTY\""
+        __json += ",\"foo\":"
+        __json += serializeString(input: self.foo)
+        __json += ",\"bar\":"
+        __json += "\(self.bar)"
+        __json += ",\"baz\":"
+        __json += "\(self.baz)"
+        __json += "}"
+        return __json
+    }
+    public func toURLQueryParts() -> [URLQueryItem] {
+        var __queryParts: [URLQueryItem] = []
+        __queryParts.append(URLQueryItem(name: "type", value: "NOT_EMPTY"))
+        __queryParts.append(URLQueryItem(name: "foo", value: self.foo))
+        __queryParts.append(URLQueryItem(name: "bar", value: "\(self.bar)"))
+        __queryParts.append(URLQueryItem(name: "baz", value: "\(self.baz)"))
+        return __queryParts
+    }
+    public func clone() -> DiscriminatorWithEmptyObjectNotEmpty {
+
+        return DiscriminatorWithEmptyObjectNotEmpty(
+            foo: self.foo,
+            bar: self.bar,
+            baz: self.baz
+        )
+    }
+    
+}
+    
+
 public struct SendErrorParams: ArriClientModel {
     public var code: UInt16 = 0
     public var message: String = ""
diff --git a/tests/clients/swift/Sources/TestClientPrefixed.g.swift b/tests/clients/swift/Sources/TestClientPrefixed.g.swift
index 4f78bd8c..5d157753 100644
--- a/tests/clients/swift/Sources/TestClientPrefixed.g.swift
+++ b/tests/clients/swift/Sources/TestClientPrefixed.g.swift
@@ -117,6 +117,18 @@ public class TestClientPrefixedTestsService {
         )
         
     }
+    public func sendDiscriminatorWithEmptyObject(_ params: FooDiscriminatorWithEmptyObject) async throws -> FooDiscriminatorWithEmptyObject {
+        let result: FooDiscriminatorWithEmptyObject = try await parsedArriHttpRequest(
+            delegate: self.delegate,
+            url: "\(self.baseURL)/rpcs/tests/send-discriminator-with-empty-object",
+            method: "POST",
+            headers: self.headers,
+            clientVersion: "10",
+            params: params,
+            onError: onError
+        )
+        return result
+    }
     public func sendError(_ params: FooSendErrorParams) async throws -> () {
         let _: EmptyArriModel = try await parsedArriHttpRequest(
             delegate: self.delegate,
@@ -506,6 +518,192 @@ public struct FooDeprecatedRpcParams: ArriClientModel {
 }
     
 
+public enum FooDiscriminatorWithEmptyObject: ArriClientModel {
+    case empty(FooDiscriminatorWithEmptyObjectEmpty)
+    case notEmpty(FooDiscriminatorWithEmptyObjectNotEmpty)
+    public init() {
+        self = .empty(FooDiscriminatorWithEmptyObjectEmpty())
+    }
+    public init(json: JSON) {
+        let discriminator = json["type"].string ?? ""
+        switch (discriminator) {
+            case "EMPTY":
+                self = .empty(FooDiscriminatorWithEmptyObjectEmpty(json: json))
+                break
+            case "NOT_EMPTY":
+                self = .notEmpty(FooDiscriminatorWithEmptyObjectNotEmpty(json: json))
+                break
+            default:
+                self = .empty(FooDiscriminatorWithEmptyObjectEmpty())
+                break
+        }
+    }
+    public init(JSONData: Data) {
+        do {
+            let json = try JSON(data: JSONData)
+            self.init(json: json)
+        } catch {
+            print("[WARNING] Error parsing JSON: \(error)")
+            self.init()
+        }
+    }
+    public init(JSONString: String) {
+        do {
+            let json = try JSON(data: JSONString.data(using: .utf8) ?? Data())
+            self.init(json: json)
+        } catch {
+            print("[WARNING] Error parsing JSON: \(error)")
+            self.init()
+        }
+    }
+    public func toJSONString() -> String {
+        switch(self) {
+            case .empty(let __innerVal):
+                return __innerVal.toJSONString()
+            case .notEmpty(let __innerVal):
+                return __innerVal.toJSONString()
+        }
+    }
+    public func toURLQueryParts() -> [URLQueryItem] {
+        switch(self) {
+            case .empty(let __innerVal):
+                return __innerVal.toURLQueryParts()
+            case .notEmpty(let __innerVal):
+                return __innerVal.toURLQueryParts()
+        }
+    }
+    public func clone() -> FooDiscriminatorWithEmptyObject {
+        switch(self) {
+            case .empty(let __innerVal):
+                return .empty(__innerVal.clone())
+            case .notEmpty(let __innerVal):
+                return .notEmpty(__innerVal.clone())
+        }
+    }
+}
+    
+public struct FooDiscriminatorWithEmptyObjectEmpty: ArriClientModel {
+    let type: String = "EMPTY"
+    public init(
+
+    ) {
+
+    }
+    
+    public init(json: JSON) {
+
+    }
+    public init(JSONData: Data) {
+        do {
+            let json = try JSON(data: JSONData)
+            self.init(json: json)
+        } catch {
+            print("[WARNING] Error parsing JSON: \(error)")
+            self.init()
+        }
+    }
+    public init(JSONString: String) {
+        do {
+            let json = try JSON(data: JSONString.data(using: .utf8) ?? Data())
+            self.init(json: json) 
+        } catch {
+            print("[WARNING] Error parsing JSON: \(error)")
+            self.init()
+        }
+    }
+    public func toJSONString() -> String {
+        var __json = "{"
+
+        __json += "\"type\":\"EMPTY\""
+        __json += "}"
+        return __json
+    }
+    public func toURLQueryParts() -> [URLQueryItem] {
+        var __queryParts: [URLQueryItem] = []
+        __queryParts.append(URLQueryItem(name: "type", value: "EMPTY"))
+        return __queryParts
+    }
+    public func clone() -> FooDiscriminatorWithEmptyObjectEmpty {
+
+        return FooDiscriminatorWithEmptyObjectEmpty(
+
+        )
+    }
+    
+}
+    
+
+public struct FooDiscriminatorWithEmptyObjectNotEmpty: ArriClientModel {
+    let type: String = "NOT_EMPTY"
+    public var foo: String = ""
+    public var bar: Float64 = 0.0
+    public var baz: Bool = false
+    public init(
+        foo: String,
+        bar: Float64,
+        baz: Bool
+    ) {
+            self.foo = foo
+            self.bar = bar
+            self.baz = baz
+    }
+    public init() {}
+    public init(json: JSON) {
+        self.foo = json["foo"].string ?? ""
+        self.bar = json["bar"].double ?? 0.0
+        self.baz = json["baz"].bool ?? false
+    }
+    public init(JSONData: Data) {
+        do {
+            let json = try JSON(data: JSONData)
+            self.init(json: json)
+        } catch {
+            print("[WARNING] Error parsing JSON: \(error)")
+            self.init()
+        }
+    }
+    public init(JSONString: String) {
+        do {
+            let json = try JSON(data: JSONString.data(using: .utf8) ?? Data())
+            self.init(json: json) 
+        } catch {
+            print("[WARNING] Error parsing JSON: \(error)")
+            self.init()
+        }
+    }
+    public func toJSONString() -> String {
+        var __json = "{"
+
+        __json += "\"type\":\"NOT_EMPTY\""
+        __json += ",\"foo\":"
+        __json += serializeString(input: self.foo)
+        __json += ",\"bar\":"
+        __json += "\(self.bar)"
+        __json += ",\"baz\":"
+        __json += "\(self.baz)"
+        __json += "}"
+        return __json
+    }
+    public func toURLQueryParts() -> [URLQueryItem] {
+        var __queryParts: [URLQueryItem] = []
+        __queryParts.append(URLQueryItem(name: "type", value: "NOT_EMPTY"))
+        __queryParts.append(URLQueryItem(name: "foo", value: self.foo))
+        __queryParts.append(URLQueryItem(name: "bar", value: "\(self.bar)"))
+        __queryParts.append(URLQueryItem(name: "baz", value: "\(self.baz)"))
+        return __queryParts
+    }
+    public func clone() -> FooDiscriminatorWithEmptyObjectNotEmpty {
+
+        return FooDiscriminatorWithEmptyObjectNotEmpty(
+            foo: self.foo,
+            bar: self.bar,
+            baz: self.baz
+        )
+    }
+    
+}
+    
+
 public struct FooSendErrorParams: ArriClientModel {
     public var code: UInt16 = 0
     public var message: String = ""
diff --git a/tests/clients/swift/project.json b/tests/clients/swift/project.json
index b7fc8d8c..2684c54c 100644
--- a/tests/clients/swift/project.json
+++ b/tests/clients/swift/project.json
@@ -11,6 +11,7 @@
       }
     },
     "compile": {
+      "dependsOn": ["^compile"],
       "executor": "nx:run-commands",
       "options": {
         "command": "swift build",
diff --git a/tests/clients/ts/testClient.g.ts b/tests/clients/ts/testClient.g.ts
index bb6f4344..83bf0f28 100644
--- a/tests/clients/ts/testClient.g.ts
+++ b/tests/clients/ts/testClient.g.ts
@@ -137,6 +137,24 @@ export class TestClientTestsService {
             clientVersion: '10',
         });
     }
+    async sendDiscriminatorWithEmptyObject(
+        params: DiscriminatorWithEmptyObject,
+    ): Promise<DiscriminatorWithEmptyObject> {
+        return arriRequest<
+            DiscriminatorWithEmptyObject,
+            DiscriminatorWithEmptyObject
+        >({
+            url: `${this._baseUrl}/rpcs/tests/send-discriminator-with-empty-object`,
+            method: 'post',
+            headers: this._headers,
+            onError: this._onError,
+            params: params,
+            responseFromJson: $$DiscriminatorWithEmptyObject.fromJson,
+            responseFromString: $$DiscriminatorWithEmptyObject.fromJsonString,
+            serializer: $$DiscriminatorWithEmptyObject.toJsonString,
+            clientVersion: '10',
+        });
+    }
     async sendError(params: SendErrorParams): Promise<undefined> {
         return arriRequest<undefined, SendErrorParams>({
             url: `${this._baseUrl}/rpcs/tests/send-error`,
@@ -555,6 +573,192 @@ export const $$DeprecatedRpcParams: ArriModelValidator<DeprecatedRpcParams> = {
     },
 };
 
+export type DiscriminatorWithEmptyObject =
+    | DiscriminatorWithEmptyObjectEmpty
+    | DiscriminatorWithEmptyObjectNotEmpty;
+export const $$DiscriminatorWithEmptyObject: ArriModelValidator<DiscriminatorWithEmptyObject> =
+    {
+        new(): DiscriminatorWithEmptyObject {
+            return $$DiscriminatorWithEmptyObjectEmpty.new();
+        },
+        validate(input): input is DiscriminatorWithEmptyObject {
+            if (!isObject(input)) {
+                return false;
+            }
+            if (typeof input.type !== 'string') {
+                return false;
+            }
+            switch (input.type) {
+                case 'EMPTY':
+                    return $$DiscriminatorWithEmptyObjectEmpty.validate(input);
+                case 'NOT_EMPTY':
+                    return $$DiscriminatorWithEmptyObjectNotEmpty.validate(
+                        input,
+                    );
+                default:
+                    return false;
+            }
+        },
+        fromJson(input): DiscriminatorWithEmptyObject {
+            switch (input.type) {
+                case 'EMPTY':
+                    return $$DiscriminatorWithEmptyObjectEmpty.fromJson(input);
+                case 'NOT_EMPTY':
+                    return $$DiscriminatorWithEmptyObjectNotEmpty.fromJson(
+                        input,
+                    );
+                default:
+                    return $$DiscriminatorWithEmptyObjectEmpty.new();
+            }
+        },
+        fromJsonString(input): DiscriminatorWithEmptyObject {
+            return $$DiscriminatorWithEmptyObject.fromJson(JSON.parse(input));
+        },
+        toJsonString(input): string {
+            switch (input.type) {
+                case 'EMPTY':
+                    return $$DiscriminatorWithEmptyObjectEmpty.toJsonString(
+                        input,
+                    );
+                case 'NOT_EMPTY':
+                    return $$DiscriminatorWithEmptyObjectNotEmpty.toJsonString(
+                        input,
+                    );
+                default:
+                    throw new Error(`Unhandled case "${(input as any).type}"`);
+            }
+        },
+        toUrlQueryString(input): string {
+            switch (input.type) {
+                case 'EMPTY':
+                    return $$DiscriminatorWithEmptyObjectEmpty.toUrlQueryString(
+                        input,
+                    );
+                case 'NOT_EMPTY':
+                    return $$DiscriminatorWithEmptyObjectNotEmpty.toUrlQueryString(
+                        input,
+                    );
+                default:
+                    throw new Error('Unhandled case');
+            }
+        },
+    };
+export interface DiscriminatorWithEmptyObjectEmpty {
+    type: 'EMPTY';
+}
+const $$DiscriminatorWithEmptyObjectEmpty: ArriModelValidator<DiscriminatorWithEmptyObjectEmpty> =
+    {
+        new(): DiscriminatorWithEmptyObjectEmpty {
+            return {
+                type: 'EMPTY',
+            };
+        },
+        validate(input): input is DiscriminatorWithEmptyObjectEmpty {
+            return isObject(input) && input.type === 'EMPTY';
+        },
+        fromJson(input): DiscriminatorWithEmptyObjectEmpty {
+            const _type = 'EMPTY';
+            return {
+                type: _type,
+            };
+        },
+        fromJsonString(input): DiscriminatorWithEmptyObjectEmpty {
+            return $$DiscriminatorWithEmptyObjectEmpty.fromJson(
+                JSON.parse(input),
+            );
+        },
+        toJsonString(input): string {
+            let json = '{';
+            json += '"type":"EMPTY"';
+            json += '}';
+            return json;
+        },
+        toUrlQueryString(input): string {
+            const queryParts: string[] = [];
+            queryParts.push('type=EMPTY');
+            return queryParts.join('&');
+        },
+    };
+
+export interface DiscriminatorWithEmptyObjectNotEmpty {
+    type: 'NOT_EMPTY';
+    foo: string;
+    bar: number;
+    baz: boolean;
+}
+const $$DiscriminatorWithEmptyObjectNotEmpty: ArriModelValidator<DiscriminatorWithEmptyObjectNotEmpty> =
+    {
+        new(): DiscriminatorWithEmptyObjectNotEmpty {
+            return {
+                type: 'NOT_EMPTY',
+                foo: '',
+                bar: 0,
+                baz: false,
+            };
+        },
+        validate(input): input is DiscriminatorWithEmptyObjectNotEmpty {
+            return (
+                isObject(input) &&
+                input.type === 'NOT_EMPTY' &&
+                typeof input.foo === 'string' &&
+                typeof input.bar === 'number' &&
+                typeof input.baz === 'boolean'
+            );
+        },
+        fromJson(input): DiscriminatorWithEmptyObjectNotEmpty {
+            const _type = 'NOT_EMPTY';
+            let _foo: string;
+            if (typeof input.foo === 'string') {
+                _foo = input.foo;
+            } else {
+                _foo = '';
+            }
+            let _bar: number;
+            if (typeof input.bar === 'number') {
+                _bar = input.bar;
+            } else {
+                _bar = 0;
+            }
+            let _baz: boolean;
+            if (typeof input.baz === 'boolean') {
+                _baz = input.baz;
+            } else {
+                _baz = false;
+            }
+            return {
+                type: _type,
+                foo: _foo,
+                bar: _bar,
+                baz: _baz,
+            };
+        },
+        fromJsonString(input): DiscriminatorWithEmptyObjectNotEmpty {
+            return $$DiscriminatorWithEmptyObjectNotEmpty.fromJson(
+                JSON.parse(input),
+            );
+        },
+        toJsonString(input): string {
+            let json = '{';
+            json += '"type":"NOT_EMPTY"';
+            json += ',"foo":';
+            json += serializeString(input.foo);
+            json += ',"bar":';
+            json += `${input.bar}`;
+            json += ',"baz":';
+            json += `${input.baz}`;
+            json += '}';
+            return json;
+        },
+        toUrlQueryString(input): string {
+            const queryParts: string[] = [];
+            queryParts.push('type=NOT_EMPTY');
+            queryParts.push(`foo=${input.foo}`);
+            queryParts.push(`bar=${input.bar}`);
+            queryParts.push(`baz=${input.baz}`);
+            return queryParts.join('&');
+        },
+    };
+
 export interface SendErrorParams {
     code: number;
     message: string;
diff --git a/tests/clients/ts/tsClientPrefixed.g.ts b/tests/clients/ts/tsClientPrefixed.g.ts
index 7e5b965c..674f76e3 100644
--- a/tests/clients/ts/tsClientPrefixed.g.ts
+++ b/tests/clients/ts/tsClientPrefixed.g.ts
@@ -135,6 +135,24 @@ export class TestClientPrefixedTestsService {
       clientVersion: "10",
     });
   }
+  async sendDiscriminatorWithEmptyObject(
+    params: FooDiscriminatorWithEmptyObject,
+  ): Promise<FooDiscriminatorWithEmptyObject> {
+    return arriRequest<
+      FooDiscriminatorWithEmptyObject,
+      FooDiscriminatorWithEmptyObject
+    >({
+      url: `${this._baseUrl}/rpcs/tests/send-discriminator-with-empty-object`,
+      method: "post",
+      headers: this._headers,
+      onError: this._onError,
+      params: params,
+      responseFromJson: $$FooDiscriminatorWithEmptyObject.fromJson,
+      responseFromString: $$FooDiscriminatorWithEmptyObject.fromJsonString,
+      serializer: $$FooDiscriminatorWithEmptyObject.toJsonString,
+      clientVersion: "10",
+    });
+  }
   async sendError(params: FooSendErrorParams): Promise<undefined> {
     return arriRequest<undefined, FooSendErrorParams>({
       url: `${this._baseUrl}/rpcs/tests/send-error`,
@@ -558,6 +576,180 @@ export const $$FooDeprecatedRpcParams: ArriModelValidator<FooDeprecatedRpcParams
     },
   };
 
+export type FooDiscriminatorWithEmptyObject =
+  | FooDiscriminatorWithEmptyObjectEmpty
+  | FooDiscriminatorWithEmptyObjectNotEmpty;
+export const $$FooDiscriminatorWithEmptyObject: ArriModelValidator<FooDiscriminatorWithEmptyObject> =
+  {
+    new(): FooDiscriminatorWithEmptyObject {
+      return $$FooDiscriminatorWithEmptyObjectEmpty.new();
+    },
+    validate(input): input is FooDiscriminatorWithEmptyObject {
+      if (!isObject(input)) {
+        return false;
+      }
+      if (typeof input.type !== "string") {
+        return false;
+      }
+      switch (input.type) {
+        case "EMPTY":
+          return $$FooDiscriminatorWithEmptyObjectEmpty.validate(input);
+        case "NOT_EMPTY":
+          return $$FooDiscriminatorWithEmptyObjectNotEmpty.validate(input);
+        default:
+          return false;
+      }
+    },
+    fromJson(input): FooDiscriminatorWithEmptyObject {
+      switch (input.type) {
+        case "EMPTY":
+          return $$FooDiscriminatorWithEmptyObjectEmpty.fromJson(input);
+        case "NOT_EMPTY":
+          return $$FooDiscriminatorWithEmptyObjectNotEmpty.fromJson(input);
+        default:
+          return $$FooDiscriminatorWithEmptyObjectEmpty.new();
+      }
+    },
+    fromJsonString(input): FooDiscriminatorWithEmptyObject {
+      return $$FooDiscriminatorWithEmptyObject.fromJson(JSON.parse(input));
+    },
+    toJsonString(input): string {
+      switch (input.type) {
+        case "EMPTY":
+          return $$FooDiscriminatorWithEmptyObjectEmpty.toJsonString(input);
+        case "NOT_EMPTY":
+          return $$FooDiscriminatorWithEmptyObjectNotEmpty.toJsonString(input);
+        default:
+          throw new Error(`Unhandled case "${(input as any).type}"`);
+      }
+    },
+    toUrlQueryString(input): string {
+      switch (input.type) {
+        case "EMPTY":
+          return $$FooDiscriminatorWithEmptyObjectEmpty.toUrlQueryString(input);
+        case "NOT_EMPTY":
+          return $$FooDiscriminatorWithEmptyObjectNotEmpty.toUrlQueryString(
+            input,
+          );
+        default:
+          throw new Error("Unhandled case");
+      }
+    },
+  };
+export interface FooDiscriminatorWithEmptyObjectEmpty {
+  type: "EMPTY";
+}
+const $$FooDiscriminatorWithEmptyObjectEmpty: ArriModelValidator<FooDiscriminatorWithEmptyObjectEmpty> =
+  {
+    new(): FooDiscriminatorWithEmptyObjectEmpty {
+      return {
+        type: "EMPTY",
+      };
+    },
+    validate(input): input is FooDiscriminatorWithEmptyObjectEmpty {
+      return isObject(input) && input.type === "EMPTY";
+    },
+    fromJson(input): FooDiscriminatorWithEmptyObjectEmpty {
+      const _type = "EMPTY";
+      return {
+        type: _type,
+      };
+    },
+    fromJsonString(input): FooDiscriminatorWithEmptyObjectEmpty {
+      return $$FooDiscriminatorWithEmptyObjectEmpty.fromJson(JSON.parse(input));
+    },
+    toJsonString(input): string {
+      let json = "{";
+      json += '"type":"EMPTY"';
+      json += "}";
+      return json;
+    },
+    toUrlQueryString(input): string {
+      const queryParts: string[] = [];
+      queryParts.push("type=EMPTY");
+      return queryParts.join("&");
+    },
+  };
+
+export interface FooDiscriminatorWithEmptyObjectNotEmpty {
+  type: "NOT_EMPTY";
+  foo: string;
+  bar: number;
+  baz: boolean;
+}
+const $$FooDiscriminatorWithEmptyObjectNotEmpty: ArriModelValidator<FooDiscriminatorWithEmptyObjectNotEmpty> =
+  {
+    new(): FooDiscriminatorWithEmptyObjectNotEmpty {
+      return {
+        type: "NOT_EMPTY",
+        foo: "",
+        bar: 0,
+        baz: false,
+      };
+    },
+    validate(input): input is FooDiscriminatorWithEmptyObjectNotEmpty {
+      return (
+        isObject(input) &&
+        input.type === "NOT_EMPTY" &&
+        typeof input.foo === "string" &&
+        typeof input.bar === "number" &&
+        typeof input.baz === "boolean"
+      );
+    },
+    fromJson(input): FooDiscriminatorWithEmptyObjectNotEmpty {
+      const _type = "NOT_EMPTY";
+      let _foo: string;
+      if (typeof input.foo === "string") {
+        _foo = input.foo;
+      } else {
+        _foo = "";
+      }
+      let _bar: number;
+      if (typeof input.bar === "number") {
+        _bar = input.bar;
+      } else {
+        _bar = 0;
+      }
+      let _baz: boolean;
+      if (typeof input.baz === "boolean") {
+        _baz = input.baz;
+      } else {
+        _baz = false;
+      }
+      return {
+        type: _type,
+        foo: _foo,
+        bar: _bar,
+        baz: _baz,
+      };
+    },
+    fromJsonString(input): FooDiscriminatorWithEmptyObjectNotEmpty {
+      return $$FooDiscriminatorWithEmptyObjectNotEmpty.fromJson(
+        JSON.parse(input),
+      );
+    },
+    toJsonString(input): string {
+      let json = "{";
+      json += '"type":"NOT_EMPTY"';
+      json += ',"foo":';
+      json += serializeString(input.foo);
+      json += ',"bar":';
+      json += `${input.bar}`;
+      json += ',"baz":';
+      json += `${input.baz}`;
+      json += "}";
+      return json;
+    },
+    toUrlQueryString(input): string {
+      const queryParts: string[] = [];
+      queryParts.push("type=NOT_EMPTY");
+      queryParts.push(`foo=${input.foo}`);
+      queryParts.push(`bar=${input.bar}`);
+      queryParts.push(`baz=${input.baz}`);
+      return queryParts.join("&");
+    },
+  };
+
 export interface FooSendErrorParams {
   code: number;
   message: string;
diff --git a/tests/servers/go/main.go b/tests/servers/go/main.go
index 9b7a7e50..2ad010ae 100644
--- a/tests/servers/go/main.go
+++ b/tests/servers/go/main.go
@@ -79,6 +79,7 @@ func main() {
 		Description:  "If the target language supports it. Generated code should mark this procedure as deprecated.",
 	})
 	arri.RegisterDef(&app, DeprecatedRpcParams{}, arri.DefOptions{IsDeprecated: true})
+	arri.ScopedRpc(&app, "tests", SendDiscriminatorWithEmptyObject, arri.RpcOptions{})
 	arri.ScopedRpc(&app, "tests", SendError, arri.RpcOptions{})
 	arri.ScopedRpc(&app, "tests", SendObject, arri.RpcOptions{})
 	arri.ScopedRpc(&app, "tests", SendObjectWithNullableFields, arri.RpcOptions{})
@@ -109,6 +110,23 @@ func DeprecatedRpc(_ DeprecatedRpcParams, _ RpcEvent) (arri.EmptyMessage, arri.R
 	return arri.EmptyMessage{}, nil
 }
 
+type DiscriminatorWithEmptyObject struct {
+	Empty    *DiscriminatorWithEmptyObjectEmpty    `discriminator:"EMPTY"`
+	NotEmpty *DiscriminatorWithEmptyObjectNotEmpty `discriminator:"NOT_EMPTY"`
+}
+
+type DiscriminatorWithEmptyObjectEmpty struct{}
+
+type DiscriminatorWithEmptyObjectNotEmpty struct {
+	Foo string
+	Bar float64
+	Baz bool
+}
+
+func SendDiscriminatorWithEmptyObject(params DiscriminatorWithEmptyObject, _ RpcEvent) (DiscriminatorWithEmptyObject, arri.RpcError) {
+	return params, nil
+}
+
 type DefaultPayload struct {
 	Message string
 }
diff --git a/tests/servers/ts/src/procedures/tests/sendDiscriminatorWithEmptyObject.rpc.ts b/tests/servers/ts/src/procedures/tests/sendDiscriminatorWithEmptyObject.rpc.ts
new file mode 100644
index 00000000..9d82e63d
--- /dev/null
+++ b/tests/servers/ts/src/procedures/tests/sendDiscriminatorWithEmptyObject.rpc.ts
@@ -0,0 +1,23 @@
+import { a } from '@arrirpc/schema';
+import { defineRpc } from '@arrirpc/server';
+
+const DiscriminatorWithEmptyObject = a.discriminator(
+    'DiscriminatorWithEmptyObject',
+    'type',
+    {
+        EMPTY: a.object({}),
+        NOT_EMPTY: a.object({
+            foo: a.string(),
+            bar: a.float64(),
+            baz: a.boolean(),
+        }),
+    },
+);
+
+export default defineRpc({
+    params: DiscriminatorWithEmptyObject,
+    response: DiscriminatorWithEmptyObject,
+    handler({ params }) {
+        return params;
+    },
+});
diff --git a/tests/test-files/AppDefinition.json b/tests/test-files/AppDefinition.json
index 4e4b5090..43fe8621 100644
--- a/tests/test-files/AppDefinition.json
+++ b/tests/test-files/AppDefinition.json
@@ -43,6 +43,7 @@
     }
   },
   "definitions": {
+    "EmptyObject": { "properties": {}, "metadata": {} },
     "Book": {
       "properties": {
         "id": {
@@ -275,8 +276,8 @@
     },
     "RecursiveObject": {
       "properties": {
-        "left": { "ref": "RecursiveObject", "metadata": {}, "nullable": true },
-        "right": { "ref": "RecursiveObject", "metadata": {}, "nullable": true }
+        "left": { "ref": "RecursiveObject", "nullable": true, "metadata": {} },
+        "right": { "ref": "RecursiveObject", "nullable": true, "metadata": {} }
       },
       "metadata": { "id": "RecursiveObject" }
     }
diff --git a/tests/test-files/EmptyObject.json b/tests/test-files/EmptyObject.json
new file mode 100644
index 00000000..9e26dfee
--- /dev/null
+++ b/tests/test-files/EmptyObject.json
@@ -0,0 +1,1 @@
+{}
\ No newline at end of file
diff --git a/tests/test-files/README.md b/tests/test-files/README.md
index fe1c4e26..d316c7ce 100644
--- a/tests/test-files/README.md
+++ b/tests/test-files/README.md
@@ -1,5 +1,11 @@
 Below are all of the contents of the test JSON files in an easier to read format. Since all of the test files are minified.
 
+## EmptyObject.json
+
+```json
+{}
+```
+
 ## Book.json
 
 ```json